first commit

This commit is contained in:
huangzhenpc
2025-06-07 15:35:13 +08:00
parent 4e14f40dbf
commit 669705dc9a
115 changed files with 19831 additions and 0 deletions

15
.bumpversion.cfg Normal file
View File

@@ -0,0 +1,15 @@
[bumpversion]
current_version = 2.2.5
commit = False
tag = False
allow_dirty = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
serialize = {major}.{minor}.{patch}
[bumpversion:file:pyproject.toml]
search = version = "{current_version}"
replace = version = "{new_version}"
[bumpversion:file:src/mcp_feedback_enhanced/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"

276
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,276 @@
name: Auto Release to PyPI
on:
workflow_dispatch:
inputs:
version_type:
description: 'Version bump type'
required: true
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)
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install
- name: Install dependencies
run: |
uv sync --dev
- name: Configure Git
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: Commit dependency changes if any
run: |
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "📦 Update dependencies" || true
fi
- name: Get current version
id: current_version
run: |
CURRENT_VERSION=$(grep '^version =' pyproject.toml | cut -d'"' -f2)
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Current version: $CURRENT_VERSION"
- name: Bump version
id: bump_version
run: |
uv run bump2version --allow-dirty ${{ github.event.inputs.version_type }}
NEW_VERSION=$(grep '^version =' pyproject.toml | cut -d'"' -f2)
echo "new=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"
- name: Update __init__.py version
run: |
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
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
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
### 🇺🇸 English
EOF
# Add English content
cat "$RELEASE_DIR/en.md" >> release_body.md
# Add separator
cat >> release_body.md << 'EOF'
---
### 🇹🇼 繁體中文
EOF
# Add Traditional Chinese content
cat "$RELEASE_DIR/zh-TW.md" >> release_body.md
# Add separator
cat >> release_body.md << 'EOF'
---
### 🇨🇳 简体中文
EOF
# Add Simplified Chinese content
cat "$RELEASE_DIR/zh-CN.md" >> release_body.md
# Add installation section
cat >> release_body.md << 'EOF'
---
## 📦 Installation & Update
```bash
# Quick test latest version
uvx mcp-feedback-enhanced@latest test
# Update to this specific version
EOF
echo "uvx mcp-feedback-enhanced@$NEW_VERSION test" >> 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** 🤖
EOF
echo "Release body generated successfully"
- name: Sync 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
if [ -f "RELEASE_NOTES/CHANGELOG.en.md" ]; then
add_to_changelog "$RELEASE_DIR/en.md" "RELEASE_NOTES/CHANGELOG.en.md"
else
echo "⚠️ CHANGELOG.en.md not found, skipping"
fi
if [ -f "RELEASE_NOTES/CHANGELOG.zh-TW.md" ]; then
add_to_changelog "$RELEASE_DIR/zh-TW.md" "RELEASE_NOTES/CHANGELOG.zh-TW.md"
else
echo "⚠️ CHANGELOG.zh-TW.md not found, skipping"
fi
if [ -f "RELEASE_NOTES/CHANGELOG.zh-CN.md" ]; then
add_to_changelog "$RELEASE_DIR/zh-CN.md" "RELEASE_NOTES/CHANGELOG.zh-CN.md"
else
echo "⚠️ CHANGELOG.zh-CN.md not found, skipping"
fi
echo "📝 CHANGELOG files synchronized"
- name: Commit version bump and changelog updates
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"
git tag "v${{ steps.bump_version.outputs.new }}"
- name: Build package
run: uv build
- name: Check package
run: uv run twine check dist/*
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
- name: Push changes and tags
run: |
git push origin main
git push origin "v${{ steps.bump_version.outputs.new }}"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ steps.bump_version.outputs.new }}"
name: "Release v${{ steps.bump_version.outputs.new }}"
body_path: release_body.md
draft: false
prerelease: false
generate_release_notes: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
run: |
echo "🎉 Release v${{ steps.bump_version.outputs.new }} completed successfully!"
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 ""
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"

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv*/
venv*/
# Logs
*.log
#Others
.DS_Store
.cursor/rules/
uv.lock
.mcp_feedback_settings.json
test_reports/

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
MIT License
Copyright (c) 2024 Fábio Ferreira
Portions of this software are modifications and enhancements
Copyright (c) 2024 Minidoracat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

259
README.zh-CN.md Normal file
View File

@@ -0,0 +1,259 @@
# MCP Feedback Enhanced交互反馈 MCP
**🌐 语言切换 / Language:** [English](README.md) | [繁體中文](README.zh-TW.md) | **简体中文**
**原作者:** [Fábio Ferreira](https://x.com/fabiomlferreira) | [原始项目](https://github.com/noopstudios/interactive-feedback-mcp) ⭐
**分支版本:** [Minidoracat](https://github.com/Minidoracat)
**UI 设计参考:** [sanshao85/mcp-feedback-collector](https://github.com/sanshao85/mcp-feedback-collector)
## 🎯 核心概念
这是一个 [MCP 服务器](https://modelcontextprotocol.io/),建立**反馈导向的开发工作流程**,完美适配本地、**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)
### 🔄 工作流程
1. **AI 调用**`mcp-feedback-enhanced`
2. **环境检测** → 自动选择合适界面
3. **用户交互** → 命令执行、文字反馈、图片上传
4. **反馈传递** → 信息返回 AI
5. **流程继续** → 根据反馈调整或结束
## 🌟 主要功能
### 🖥️ 双界面系统
- **Qt GUI**:本地环境原生体验,模块化重构设计
- **Web UI**:远程 SSH 环境与 WSL 环境现代化界面,全新架构
- **智能切换**:自动检测环境(本地/远程/WSL并选择最适界面
### 🎨 全新界面设计v2.1.0
- **模块化架构**GUI 和 Web UI 均采用模块化设计
- **集中管理**:文件夹结构重新组织,维护更容易
- **现代化主题**:改进的视觉设计和用户体验
- **响应式布局**:适应不同屏幕尺寸和窗口大小
### 🖼️ 图片支持
- **格式支持**PNG、JPG、JPEG、GIF、BMP、WebP
- **上传方式**:拖拽文件 + 剪贴板粘贴Ctrl+V
- **自动处理**:智能压缩确保符合 1MB 限制
### 🌏 多语言
- **三语支持**:简体中文、英文、繁体中文
- **智能检测**:根据系统语言自动选择
- **即时切换**:界面内可直接切换语言
### ✨ WSL 环境支持v2.2.5 新功能)
- **自动检测**:智能识别 WSL (Windows Subsystem for Linux) 环境
- **浏览器整合**WSL 环境下自动启动 Windows 浏览器
- **多种启动方式**:支持 `cmd.exe``powershell.exe``wslview` 等多种浏览器启动方法
- **无缝体验**WSL 用户可直接使用 Web UI无需额外配置
## 🖥️ 界面预览
### Qt GUI 界面(重构版)
<div align="center">
<img src="docs/zh-CN/images/gui1.png" width="400" alt="Qt GUI 主界面" />
<img src="docs/zh-CN/images/gui2.png" width="400" alt="Qt GUI 设置界面" />
</div>
*Qt GUI 界面 - 模块化重构,支持本地环境*
### Web UI 界面(重构版)
<div align="center">
<img src="docs/zh-CN/images/web1.png" width="400" alt="Web UI 主界面" />
<img src="docs/zh-CN/images/web2.png" width="400" alt="Web UI 设置界面" />
</div>
*Web UI 界面 - 全新架构,适合 SSH Remote 环境*
**快捷键支持**
- `Ctrl+Enter`Windows/Linux/ `Cmd+Enter`macOS提交反馈主键盘与数字键盘皆支持
- `Ctrl+V`Windows/Linux/ `Cmd+V`macOS直接粘贴剪贴板图片
## 🚀 快速开始
### 1. 安装与测试
```bash
# 安装 uv如果尚未安装
pip install uv
# 快速测试
uvx mcp-feedback-enhanced@latest test
```
### 2. 配置 MCP
**基本配置**(适合大多数用户):
```json
{
"mcpServers": {
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"autoApprove": ["interactive_feedback"]
}
}
}
```
**进阶配置**(需要自定义环境):
```json
{
"mcpServers": {
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"env": {
"FORCE_WEB": "true",
"MCP_DEBUG": "false"
},
"autoApprove": ["interactive_feedback"]
}
}
}
```
### 3. 设定提示工程
为了获得最佳效果,请在 AI 助手中添加以下规则:
```
# MCP Interactive Feedback 规则
1. 在任何流程、任务、对话进行时,无论是询问、回复、或完成阶段性任务,皆必须调用 MCP mcp-feedback-enhanced。
2. 每当收到用户反馈,若反馈内容非空,必须再次调用 MCP mcp-feedback-enhanced并根据反馈内容调整行为。
3. 仅当用户明确表示「结束」或「不再需要交互」时,才可停止调用 MCP mcp-feedback-enhanced流程才算结束。
4. 除非收到结束指令,否则所有步骤都必须重复调用 MCP mcp-feedback-enhanced。
5. 完成任务前,必须使用 MCP mcp-feedback-enhanced 工具向用户询问反馈。
```
## ⚙️ 进阶设定
### 环境变量
| 变量 | 用途 | 值 | 默认 |
|------|------|-----|------|
| `FORCE_WEB` | 强制使用 Web UI | `true`/`false` | `false` |
| `MCP_DEBUG` | 调试模式 | `true`/`false` | `false` |
### 测试选项
```bash
# 版本查询
uvx mcp-feedback-enhanced@latest version # 检查版本
# 指定界面测试
uvx mcp-feedback-enhanced@latest test --gui # 快速测试 Qt GUI
uvx mcp-feedback-enhanced@latest test --web # 测试 Web UI (自动持续运行)
# 调试模式
MCP_DEBUG=true uvx mcp-feedback-enhanced@latest test
```
### 开发者安装
```bash
git clone https://github.com/Minidoracat/mcp-feedback-enhanced.git
cd mcp-feedback-enhanced
uv sync
```
**本地测试方式**
```bash
# 方式一:标准测试(推荐)
uv run python -m mcp_feedback_enhanced test
# 方式二完整测试套件macOS 和 Windows 通用开发环境)
uvx --with-editable . mcp-feedback-enhanced test
# 方式三:指定界面测试
uvx --with-editable . mcp-feedback-enhanced test --gui # 快速测试 Qt GUI
uvx --with-editable . mcp-feedback-enhanced test --web # 测试 Web UI (自动持续运行)
```
**测试说明**
- **标准测试**:执行完整的功能检查,适合日常开发验证
- **完整测试**:包含所有组件的深度测试,适合发布前验证
- **Qt GUI 测试**:快速启动并测试本地图形界面
- **Web UI 测试**:启动 Web 服务器并保持运行,便于完整测试 Web 功能
## 🆕 版本更新记录
📋 **完整版本更新记录:** [RELEASE_NOTES/CHANGELOG.zh-CN.md](RELEASE_NOTES/CHANGELOG.zh-CN.md)
### 最新版本亮点v2.2.5
-**WSL 环境支持**: 新增 WSL (Windows Subsystem for Linux) 环境的完整支持
- 🌐 **智能浏览器启动**: WSL 环境下自动调用 Windows 浏览器,支持多种启动方式
- 🎯 **环境检测优化**: 改进远程环境检测逻辑WSL 不再被误判为远程环境
- 🧪 **测试体验提升**: 测试模式下自动尝试启动浏览器,提供更好的测试体验
## 🐛 常见问题
**Q: 出现 "Unexpected token 'D'" 错误**
A: 调试输出干扰。设置 `MCP_DEBUG=false` 或移除该环境变量。
**Q: 中文字符乱码**
A: 已在 v2.0.3 修复。更新到最新版本:`uvx mcp-feedback-enhanced@latest`
**Q: 图片上传失败**
A: 检查文件大小≤1MB和格式PNG/JPG/GIF/BMP/WebP
**Q: Web UI 无法启动**
A: 设置 `FORCE_WEB=true` 或检查火墙设定。
**Q: UV Cache 占用过多磁盘空间**
A: 由于频繁使用 `uvx` 命令cache 可能会累积到数十 GB。建议定期清理
```bash
# 查看 cache 大小和详细信息
python scripts/cleanup_cache.py --size
# 预览清理内容(不实际清理)
python scripts/cleanup_cache.py --dry-run
# 执行标准清理
python scripts/cleanup_cache.py --clean
# 强制清理(会尝试关闭相关程序,解决 Windows 文件占用问题)
python scripts/cleanup_cache.py --force
# 或直接使用 uv 命令
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 浏览器启动,而不会被误判为远程环境。
## 🙏 致谢
### 🌟 支持原作者
**Fábio Ferreira** - [X @fabiomlferreira](https://x.com/fabiomlferreira)
**原始项目:** [noopstudios/interactive-feedback-mcp](https://github.com/noopstudios/interactive-feedback-mcp)
如果您觉得有用,请:
- ⭐ [为原项目按星星](https://github.com/noopstudios/interactive-feedback-mcp)
- 📱 [关注原作者](https://x.com/fabiomlferreira)
### 设计灵感
**sanshao85** - [mcp-feedback-collector](https://github.com/sanshao85/mcp-feedback-collector)
### 社群支援
- **Discord** [https://discord.gg/Gur2V67](https://discord.gg/Gur2V67)
- **Issues** [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues)
## 📄 授权
MIT 授权条款 - 详见 [LICENSE](LICENSE) 档案
---
**🌟 欢迎 Star 并分享给更多开发者!**

252
README.zh-TW.md Normal file
View File

@@ -0,0 +1,252 @@
# MCP Feedback Enhanced互動回饋 MCP
**🌐 語言切換 / Language:** [English](README.md) | **繁體中文** | [简体中文](README.zh-CN.md)
**原作者:** [Fábio Ferreira](https://x.com/fabiomlferreira) | [原始專案](https://github.com/noopstudios/interactive-feedback-mcp) ⭐
**分支版本:** [Minidoracat](https://github.com/Minidoracat)
**UI 設計參考:** [sanshao85/mcp-feedback-collector](https://github.com/sanshao85/mcp-feedback-collector)
## 🎯 核心概念
這是一個 [MCP 伺服器](https://modelcontextprotocol.io/),建立**回饋導向的開發工作流程**,完美適配本地、**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)
### 🔄 工作流程
1. **AI 調用**`mcp-feedback-enhanced`
2. **環境檢測** → 自動選擇合適介面
3. **用戶互動** → 命令執行、文字回饋、圖片上傳
4. **回饋傳遞** → 資訊返回 AI
5. **流程繼續** → 根據回饋調整或結束
## 🌟 主要功能
### 🖥️ 雙介面系統
- **Qt GUI**:本地環境原生體驗,模組化重構設計
- **Web UI**:遠端 SSH 環境與 WSL 環境現代化界面,全新架構
- **智能切換**:自動檢測環境(本地/遠端/WSL並選擇最適介面
### 🎨 全新界面設計v2.1.0
- **模組化架構**GUI 和 Web UI 均採用模組化設計
- **集中管理**:資料夾結構重新組織,維護更容易
- **現代化主題**:改進的視覺設計和用戶體驗
- **響應式布局**:適應不同螢幕尺寸和視窗大小
### 🖼️ 圖片支援
- **格式支援**PNG、JPG、JPEG、GIF、BMP、WebP
- **上傳方式**:拖拽檔案 + 剪貼板粘貼Ctrl+V
- **自動處理**:智能壓縮確保符合 1MB 限制
### 🌏 多語言
- **三語支援**:繁體中文、英文、簡體中文
- **智能偵測**:根據系統語言自動選擇
- **即時切換**:介面內可直接切換語言
### ✨ WSL 環境支援v2.2.5 新功能)
- **自動檢測**:智能識別 WSL (Windows Subsystem for Linux) 環境
- **瀏覽器整合**WSL 環境下自動啟動 Windows 瀏覽器
- **多種啟動方式**:支援 `cmd.exe``powershell.exe``wslview` 等多種瀏覽器啟動方法
- **無縫體驗**WSL 用戶可直接使用 Web UI無需額外配置
## 🖥️ 介面預覽
### Qt GUI 介面(重構版)
<div align="center">
<img src="docs/zh-TW/images/gui1.png" width="400" alt="Qt GUI 主介面" />
<img src="docs/zh-TW/images/gui2.png" width="400" alt="Qt GUI 設定介面" />
</div>
*Qt GUI 介面 - 模組化重構,支援本地環境*
### Web UI 介面(重構版)
<div align="center">
<img src="docs/zh-TW/images/web1.png" width="400" alt="Web UI 主介面" />
<img src="docs/zh-TW/images/web2.png" width="400" alt="Web UI 設定介面" />
</div>
*Web UI 介面 - 全新架構,適合 SSH Remote 環境*
**快捷鍵支援**
- `Ctrl+Enter`Windows/Linux/ `Cmd+Enter`macOS提交回饋主鍵盤與數字鍵盤皆支援
- `Ctrl+V`Windows/Linux/ `Cmd+V`macOS直接粘貼剪貼板圖片
## 🚀 快速開始
### 1. 安裝與測試
```bash
# 安裝 uv如果尚未安裝
pip install uv
# 快速測試
uvx mcp-feedback-enhanced@latest test
```
### 2. 配置 MCP
**基本配置**(適合大多數用戶):
```json
{
"mcpServers": {
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"autoApprove": ["interactive_feedback"]
}
}
}
```
**進階配置**(需要自訂環境):
```json
{
"mcpServers": {
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"env": {
"FORCE_WEB": "true",
"MCP_DEBUG": "false"
},
"autoApprove": ["interactive_feedback"]
}
}
}
```
### 3. 設定提示工程
為了獲得最佳效果,請在 AI 助手中添加以下規則:
```
# MCP Interactive Feedback 規則
1. 在任何流程、任務、對話進行時,無論是詢問、回覆、或完成階段性任務,皆必須呼叫 MCP mcp-feedback-enhanced。
2. 每當收到使用者回饋,若回饋內容非空,必須再次呼叫 MCP mcp-feedback-enhanced並根據回饋內容調整行為。
3. 僅當使用者明確表示「結束」或「不再需要互動」時,才可停止呼叫 MCP mcp-feedback-enhanced流程才算結束。
4. 除非收到結束指令,否則所有步驟都必須重複呼叫 MCP mcp-feedback-enhanced。
5. 完成任務前,必須使用 MCP mcp-feedback-enhanced 工具向使用者詢問回饋。
```
## ⚙️ 進階設定
### 環境變數
| 變數 | 用途 | 值 | 默認 |
|------|------|-----|------|
| `FORCE_WEB` | 強制使用 Web UI | `true`/`false` | `false` |
| `MCP_DEBUG` | 調試模式 | `true`/`false` | `false` |
### 測試選項
```bash
# 版本查詢
uvx mcp-feedback-enhanced@latest version # 檢查版本
# 指定介面測試
uvx mcp-feedback-enhanced@latest test --gui # 快速測試 Qt GUI
uvx mcp-feedback-enhanced@latest test --web # 測試 Web UI (自動持續運行)
# 調試模式
MCP_DEBUG=true uvx mcp-feedback-enhanced@latest test
```
### 開發者安裝
```bash
git clone https://github.com/Minidoracat/mcp-feedback-enhanced.git
cd mcp-feedback-enhanced
uv sync
```
**本地測試方式**
```bash
# 方式一:標準測試(推薦)
uv run python -m mcp_feedback_enhanced test
# 方式二完整測試套件macOS 和 windows 通用開發環境)
uvx --with-editable . mcp-feedback-enhanced test
# 方式三:指定介面測試
uvx --with-editable . mcp-feedback-enhanced test --gui # 快速測試 Qt GUI
uvx --with-editable . mcp-feedback-enhanced test --web # 測試 Web UI (自動持續運行)
```
**測試說明**
- **標準測試**:執行完整的功能檢查,適合日常開發驗證
- **完整測試**:包含所有組件的深度測試,適合發布前驗證
- **Qt GUI 測試**:快速啟動並測試本地圖形界面
- **Web UI 測試**:啟動 Web 服務器並保持運行,便於完整測試 Web 功能
## 🆕 版本更新記錄
📋 **完整版本更新記錄:** [RELEASE_NOTES/CHANGELOG.zh-TW.md](RELEASE_NOTES/CHANGELOG.zh-TW.md)
### 最新版本亮點v2.2.5
-**WSL 環境支援**: 新增 WSL (Windows Subsystem for Linux) 環境的完整支援
- 🌐 **智能瀏覽器啟動**: WSL 環境下自動調用 Windows 瀏覽器,支援多種啟動方式
- 🎯 **環境檢測優化**: 改進遠端環境檢測邏輯WSL 不再被誤判為遠端環境
- 🧪 **測試體驗提升**: 測試模式下自動嘗試啟動瀏覽器,提供更好的測試體驗
## 🐛 常見問題
**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` 或檢查防火牆設定。
**Q: UV Cache 佔用過多磁碟空間**
A: 由於頻繁使用 `uvx` 命令cache 可能會累積到數十 GB。建議定期清理
```bash
# 查看 cache 大小和詳細資訊
python scripts/cleanup_cache.py --size
# 預覽清理內容(不實際清理)
python scripts/cleanup_cache.py --dry-run
# 執行標準清理
python scripts/cleanup_cache.py --clean
# 強制清理(會嘗試關閉相關程序,解決 Windows 檔案佔用問題)
python scripts/cleanup_cache.py --force
# 或直接使用 uv 命令
uv cache clean
```
詳細說明請參考:[Cache 管理指南](docs/zh-TW/cache-management.md)
**Q: AI 模型無法解析圖片**
A: 各種 AI 模型(包括 Gemini Pro 2.5、Claude 等)在圖片解析上可能存在不穩定性,表現為有時能正確識別、有時無法解析上傳的圖片內容。這是 AI 視覺理解技術的已知限制。建議:
1. 確保圖片品質良好(高對比度、清晰文字)
2. 多嘗試幾次上傳,通常重試可以成功
3. 如持續無法解析,可嘗試調整圖片大小或格式
## 🙏 致謝
### 🌟 支持原作者
**Fábio Ferreira** - [X @fabiomlferreira](https://x.com/fabiomlferreira)
**原始專案:** [noopstudios/interactive-feedback-mcp](https://github.com/noopstudios/interactive-feedback-mcp)
如果您覺得有用,請:
- ⭐ [為原專案按星星](https://github.com/noopstudios/interactive-feedback-mcp)
- 📱 [關注原作者](https://x.com/fabiomlferreira)
### 設計靈感
**sanshao85** - [mcp-feedback-collector](https://github.com/sanshao85/mcp-feedback-collector)
### 社群支援
- **Discord** [https://discord.gg/Gur2V67](https://discord.gg/Gur2V67)
- **Issues** [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues)
## 📄 授權
MIT 授權條款 - 詳見 [LICENSE](LICENSE) 檔案
---
**🌟 歡迎 Star 並分享給更多開發者!**

View File

@@ -0,0 +1,190 @@
# Changelog (English)
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
## 🌟 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)
---
### ✨ 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
---
## [v2.2.4] - GUI Experience Optimization & Bug Fixes
### 🐛 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
---
## [v2.2.3] - Timeout Control & Image Settings Enhancement
### ✨ 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
### 🐛 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
---
## [v2.2.2] - Timeout Auto-cleanup Fix
### 🐛 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
---
## [v2.2.1] - Window Optimization & Unified Settings Interface
### 🚀 Improvements
- 🖥️ **Window Size Constraint Removal**: Removed GUI main window minimum size limit from 1000×800 to 400×300
- 💾 **Real-time Window State Saving**: Implemented real-time saving mechanism for window size and position changes
- ⚙️ **Unified Settings Interface Optimization**: Improved GUI settings page configuration saving logic to avoid setting conflicts
### 🐛 Bug Fixes
- 🔧 **Window Size Constraint**: Fixed GUI window unable to resize to small dimensions issue
- 🛡️ **Setting Conflicts**: Fixed potential configuration conflicts during settings save operations
---
## [v2.2.0] - Layout & Settings UI Enhancements
### ✨ New Features
- 🎨 **Horizontal Layout Mode**: GUI & Web UI combined mode adds left-right layout option for summary and feedback
### 🚀 Improvements
- 🎨 **Improved Settings Interface**: Optimized the settings page for both GUI and Web UI
- ⌨️ **GUI Shortcut Enhancement**: Submit feedback shortcut now fully supports numeric keypad Enter key
### 🐛 Bug Fixes
- 🔧 **Image Duplication Fix**: Resolved Web UI image pasting duplication issue
---
## [v2.1.1] - Window Positioning Optimization
### ✨ New Features
- 🖥️ **Smart Window Positioning**: Added "Always show window at primary screen center" setting option
- 🌐 **Multi-Monitor Support**: Perfect solution for complex multi-monitor setups like T-shaped screen arrangements
- 💾 **Position Memory**: Auto-save and restore window position with intelligent visibility detection
---
## [v2.1.0] - Complete Refactored Version
### 🎨 Major Refactoring
- 🏗️ **Complete Refactoring**: GUI and Web UI adopt modular architecture
- 📁 **Centralized Management**: Reorganized folder structure, improved maintainability
- 🖥️ **Interface Optimization**: Modern design and improved user experience
### ✨ New Features
- 🍎 **macOS Interface Optimization**: Specialized improvements for macOS user experience
- ⚙️ **Feature Enhancement**: New settings options and auto-close page functionality
- **About Page**: Added about page with version info, project links, and acknowledgments
---
## [v2.0.14] - Shortcut & Image Feature Enhancement
### 🚀 Improvements
- ⌨️ **Enhanced Shortcuts**: Ctrl+Enter supports numeric keypad
- 🖼️ **Smart Image Pasting**: Ctrl+V directly pastes clipboard images
---
## [v2.0.9] - Multi-language Architecture Refactor
### 🔄 Refactoring
- 🌏 **Multi-language Architecture Refactor**: Support for dynamic loading
- 📁 **Modularized Language Files**: Modular organization of language files
---
## [v2.0.3] - Encoding Issues Fix
### 🐛 Critical Fixes
- 🛡️ **Complete Chinese Character Encoding Fix**: Resolved all Chinese display related issues
- 🔧 **JSON Parsing Error Fix**: Fixed data parsing errors
---
## [v2.0.0] - Web UI Support
### 🌟 Major Features
-**Added Web UI Support**: Support for remote environments
-**Auto Environment Detection**: Automatically choose appropriate interface
-**WebSocket Real-time Communication**: Real-time bidirectional communication
---
## Legend
| Icon | Meaning |
|------|---------|
| 🌟 | Version Highlights |
| ✨ | New Features |
| 🚀 | Improvements |
| 🐛 | Bug Fixes |
| 🔄 | Refactoring Changes |
| 🎨 | UI Optimization |
| ⚙️ | Settings Related |
| 🖥️ | Window Related |
| 🌐 | Multi-language/Network Related |
| 📁 | File Structure |
| ⌨️ | Shortcuts |
| 🖼️ | Image Features |
---
**Full Project Info:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced)

View File

@@ -0,0 +1,213 @@
# 更新日志 (简体中文)
本文件记录了 **MCP Feedback Enhanced** 的所有版本更新内容。
## [v2.2.5] - WSL 环境支持与跨平台增强
# 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)
---
### ✨ 新功能
- 🐧 **WSL 环境检测**: 自动识别 WSL 环境,提供专门的支持逻辑
- 🌐 **智能浏览器启动**: WSL 环境下自动调用 Windows 浏览器,支持多种启动方式
- 🔧 **跨平台测试增强**: 测试功能整合 WSL 检测,提升测试覆盖率
### 🚀 改进功能
- 🎯 **环境检测优化**: 改进远程环境检测逻辑WSL 不再被误判为远程环境
- 📊 **系统信息增强**: 系统信息工具新增 WSL 环境状态显示
- 🧪 **测试体验提升**: 测试模式下自动尝试启动浏览器,提供更好的测试体验
---
## [v2.2.4] - GUI 体验优化与问题修复
### 🐛 问题修复
- 🖼️ **图片重复粘贴修复**: 解决 GUI 界面中使用 Ctrl+V 复制粘贴图片时出现重复粘贴的问题
- 🌐 **语系切换修复**: 修复图片设定区域在语言切换时文字没有正确翻译的问题
- 📝 **字体可读性改善**: 调整图片设定区域的字体大小,提升文字可读性
---
## [v2.2.5] - WSL 环境支持与跨平台增强
### ✨ 新功能
- 🐧 **WSL 环境检测**: 自动识别 WSL 环境,提供专门的支持逻辑
- 🌐 **智能浏览器启动**: WSL 环境下自动调用 Windows 浏览器,支持多种启动方式
- 🔧 **跨平台测试增强**: 测试功能整合 WSL 检测,提升测试覆盖率
### 🚀 改进功能
- 🎯 **环境检测优化**: 改进远程环境检测逻辑WSL 不再被误判为远程环境
- 📊 **系统信息增强**: 系统信息工具新增 WSL 环境状态显示
- 🧪 **测试体验提升**: 测试模式下自动尝试启动浏览器,提供更好的测试体验
---
## [v2.2.4] - GUI 体验优化与问题修复
### 🐛 问题修复
- 🖼️ **图片重复粘贴修复**: 解决 GUI 界面中使用 Ctrl+V 复制粘贴图片时出现重复粘贴的问题
- 🌐 **语系切换修复**: 修复图片设定区域在语言切换时文字没有正确翻译的问题
- 📝 **字体可读性改善**: 调整图片设定区域的字体大小,提升文字可读性
---
## [v2.2.3] - 超时控制与图片设置增强
### ✨ 新功能
-**用户超时控制**: 新增可自定义的超时设置功能,支持 30 秒至 2 小时的弹性设置
- ⏱️ **倒数计时器**: 界面顶部显示实时倒数计时器,提供可视化的时间提醒
- 🖼️ **图片大小限制**: 新增图片上传大小限制设置(无限制/1MB/3MB/5MB
- 🔧 **Base64 兼容模式**: 新增 Base64 详细模式,提升部分 AI 模型的图片识别兼容性
- 🧹 **UV Cache 管理工具**: 新增 `cleanup_cache.py` 脚本,协助管理和清理 UV cache 空间
### 🚀 改进功能
- 📚 **文档结构优化**: 重新整理文档目录结构,将图片移至 `docs/{语言}/images/` 路径
- 📖 **Cache 管理指南**: 新增详细的 UV Cache 管理指南,包含自动化清理方案
- 🎯 **智能兼容性提示**: 当图片上传失败时自动显示 Base64 兼容模式建议
### 🐛 问题修复
- 🛡️ **超时处理优化**: 改进用户自定义超时与 MCP 系统超时的协调机制
- 🖥️ **界面自动关闭**: 修复超时后界面自动关闭和资源清理逻辑
- 📱 **响应式布局**: 优化超时控制组件在小屏幕设备上的显示效果
---
## [v2.2.2] - 超时自动清理修复
### 🐛 问题修复
- 🔄 **超时自动清理**: 修复 GUI/Web UI 在 MCP session timeout (默认 600 秒) 后没有自动关闭的问题
- 🛡️ **资源管理优化**: 改进超时处理机制,确保在超时时正确清理和关闭所有 UI 资源
-**超时检测增强**: 加强超时检测逻辑,确保在各种情况下都能正确处理超时事件
---
## [v2.2.1] - 窗口优化与统一设置接口
### 🚀 改进功能
- 🖥️ **窗口大小限制解除**: 解除 GUI 主窗口最小大小限制,从 1000×800 降至 400×300
- 💾 **窗口状态实时保存**: 实现窗口大小与位置的即时保存机制,支持防抖延迟
- ⚙️ **统一设置接口优化**: 改进 GUI 设置版面的配置保存逻辑,避免设置冲突
### 🐛 问题修复
- 🔧 **窗口大小限制**: 解决 GUI 窗口无法调整至小尺寸的问题
- 🛡️ **设置冲突**: 修复设置保存时可能出现的配置冲突问题
---
## [v2.2.0] - 布局与设置界面优化
### ✨ 新功能
- 🎨 **水平布局模式**: GUI 与 Web UI 的合并模式新增摘要与反馈的左右布局选项
### 🚀 改进功能
- 🎨 **设置界面改进**: 优化了 GUI 与 Web UI 的设置页面,提升布局清晰度
- ⌨️ **快捷键完善**: 提交反馈快捷键现已完整支持数字键盘的 Enter 键
### 🐛 问题修复
- 🔧 **图片重复粘贴**: 解决了在 Web UI 文字输入区使用 Ctrl+V 粘贴图片时的重复问题
---
## [v2.1.1] - 窗口定位优化
### ✨ 新功能
- 🖥️ **智能窗口定位**: 新增「总是在主屏幕中心显示窗口」设置选项
- 🌐 **多屏幕支持**: 完美解决 T 字型屏幕排列等复杂多屏幕环境的窗口定位问题
- 💾 **位置记忆**: 自动保存和恢复窗口位置,智能检测窗口可见性
---
## [v2.1.0] - 全面重构版
### 🎨 重大重构
- 🏗️ **全面重构**: GUI 和 Web UI 采用模块化架构
- 📁 **集中管理**: 重新组织文件夹结构,提升维护性
- 🖥️ **界面优化**: 现代化设计和改进的用户体验
### ✨ 新功能
- 🍎 **macOS 界面优化**: 针对 macOS 用户体验进行专项改进
- ⚙️ **功能增强**: 新增设置选项和自动关闭页面功能
- **关于页面**: 新增关于页面,包含版本信息、项目链接和致谢内容
---
## [v2.0.14] - 快捷键与图片功能增强
### 🚀 改进功能
- ⌨️ **增强快捷键**: Ctrl+Enter 支持数字键盘
- 🖼️ **智能图片粘贴**: Ctrl+V 直接粘贴剪贴板图片
---
## [v2.0.9] - 多语言架构重构
### 🔄 重构
- 🌏 **多语言架构重构**: 支持动态载入
- 📁 **语言文件模块化**: 模块化组织语言文件
---
## [v2.0.3] - 编码问题修复
### 🐛 重要修复
- 🛡️ **完全修复中文字符编码问题**: 解决所有中文显示相关问题
- 🔧 **解决 JSON 解析错误**: 修复数据解析错误
---
## [v2.0.0] - Web UI 支持
### 🌟 重大功能
-**新增 Web UI 支持**: 支持远程环境使用
-**自动环境检测**: 自动选择合适的界面
-**WebSocket 即时通讯**: 实现即时双向通讯
---
## 图例说明
| 图标 | 意义 |
|------|------|
| 🌟 | 版本亮点 |
| ✨ | 新功能 |
| 🚀 | 改进功能 |
| 🐛 | 问题修复 |
| 🔄 | 重构变更 |
| 🎨 | 界面优化 |
| ⚙️ | 设置相关 |
| 🖥️ | 窗口相关 |
| 🌐 | 多语言/网络相关 |
| 📁 | 文件结构 |
| ⌨️ | 快捷键 |
| 🖼️ | 图片功能 |
---
**完整项目信息:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced)

View File

@@ -0,0 +1,190 @@
# 更新日誌 (繁體中文)
本文件記錄了 **MCP Feedback Enhanced** 的所有版本更新內容。
## [v2.2.5] - WSL 環境支援與跨平台增強
# 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)
---
### ✨ 新功能
- 🐧 **WSL 環境檢測**: 自動識別 WSL 環境,提供專門的支援邏輯
- 🌐 **智能瀏覽器啟動**: WSL 環境下自動調用 Windows 瀏覽器,支援多種啟動方式
- 🔧 **跨平台測試增強**: 測試功能整合 WSL 檢測,提升測試覆蓋率
### 🚀 改進功能
- 🎯 **環境檢測優化**: 改進遠端環境檢測邏輯WSL 不再被誤判為遠端環境
- 📊 **系統資訊增強**: 系統資訊工具新增 WSL 環境狀態顯示
- 🧪 **測試體驗提升**: 測試模式下自動嘗試啟動瀏覽器,提供更好的測試體驗
---
## [v2.2.4] - GUI 體驗優化與問題修復
### 🐛 問題修復
- 🖼️ **圖片重複貼上修復**: 解決 GUI 介面中使用 Ctrl+V 複製貼上圖片時出現重複貼上的問題
- 🌐 **語系切換修復**: 修復圖片設定區域在語言切換時文字沒有正確翻譯的問題
- 📝 **字體可讀性改善**: 調整圖片設定區域的字體大小,提升文字可讀性
---
## [v2.2.3] - 超時控制與圖片設定增強
### ✨ 新功能
-**用戶超時控制**: 新增可自訂的超時設定功能,支援 30 秒至 2 小時的彈性設定
- ⏱️ **倒數計時器**: 介面頂部顯示即時倒數計時器,提供視覺化的時間提醒
- 🖼️ **圖片大小限制**: 新增圖片上傳大小限制設定(無限制/1MB/3MB/5MB
- 🔧 **Base64 相容模式**: 新增 Base64 詳細模式,提升部分 AI 模型的圖片識別相容性
- 🧹 **UV Cache 管理工具**: 新增 `cleanup_cache.py` 腳本,協助管理和清理 UV cache 空間
### 🚀 改進功能
- 📚 **文檔結構優化**: 重新整理文檔目錄結構,將圖片移至 `docs/{語言}/images/` 路徑
- 📖 **Cache 管理指南**: 新增詳細的 UV Cache 管理指南,包含自動化清理方案
- 🎯 **智能相容性提示**: 當圖片上傳失敗時自動顯示 Base64 相容模式建議
### 🐛 問題修復
- 🛡️ **超時處理優化**: 改進用戶自訂超時與 MCP 系統超時的協調機制
- 🖥️ **介面自動關閉**: 修復超時後介面自動關閉和資源清理邏輯
- 📱 **響應式佈局**: 優化超時控制元件在小螢幕設備上的顯示效果
---
## [v2.2.2] - 超時自動清理修復
### 🐛 問題修復
- 🔄 **超時自動清理**: 修復 GUI/Web UI 在 MCP session timeout (預設 600 秒) 後沒有自動關閉的問題
- 🛡️ **資源管理優化**: 改進超時處理機制,確保在超時時正確清理和關閉所有 UI 資源
-**超時檢測增強**: 加強超時檢測邏輯,確保在各種情況下都能正確處理超時事件
---
## [v2.2.1] - 視窗優化與統一設定接口
### 🚀 改進功能
- 🖥️ **視窗大小限制解除**: 解除 GUI 主視窗最小大小限制,從 1000×800 降至 400×300
- 💾 **視窗狀態實時保存**: 實現視窗大小與位置的即時保存機制,支援防抖延遲
- ⚙️ **統一設定接口優化**: 改進 GUI 設定版面的配置保存邏輯,避免設定衝突
### 🐛 問題修復
- 🔧 **視窗大小限制**: 解決 GUI 視窗無法調整至小尺寸的問題
- 🛡️ **設定衝突**: 修復設定保存時可能出現的配置衝突問題
---
## [v2.2.0] - 佈局與設定界面優化
### ✨ 新功能
- 🎨 **水平佈局模式**: GUI 與 Web UI 的合併模式新增摘要與回饋的左右佈局選項
### 🚀 改進功能
- 🎨 **設定界面改進**: 優化了 GUI 與 Web UI 的設定頁面,提升佈局清晰度
- ⌨️ **快捷鍵完善**: 提交回饋快捷鍵現已完整支援數字鍵盤的 Enter 鍵
### 🐛 問題修復
- 🔧 **圖片重複貼上**: 解決了在 Web UI 文字輸入區使用 Ctrl+V 貼上圖片時的重複問題
---
## [v2.1.1] - 視窗定位優化
### ✨ 新功能
- 🖥️ **智能視窗定位**: 新增「總是在主螢幕中心顯示視窗」設定選項
- 🌐 **多螢幕支援**: 完美解決 T 字型螢幕排列等複雜多螢幕環境的視窗定位問題
- 💾 **位置記憶**: 自動保存和恢復視窗位置,智能檢測視窗可見性
---
## [v2.1.0] - 全面重構版
### 🎨 重大重構
- 🏗️ **全面重構**: GUI 和 Web UI 採用模組化架構
- 📁 **集中管理**: 重新組織資料夾結構,提升維護性
- 🖥️ **界面優化**: 現代化設計和改進的用戶體驗
### ✨ 新功能
- 🍎 **macOS 界面優化**: 針對 macOS 用戶體驗進行專項改進
- ⚙️ **功能增強**: 新增設定選項和自動關閉頁面功能
- **關於頁面**: 新增關於頁面,包含版本資訊、專案連結和致謝內容
---
## [v2.0.14] - 快捷鍵與圖片功能增強
### 🚀 改進功能
- ⌨️ **增強快捷鍵**: Ctrl+Enter 支援數字鍵盤
- 🖼️ **智能圖片貼上**: Ctrl+V 直接貼上剪貼簿圖片
---
## [v2.0.9] - 多語言架構重構
### 🔄 重構
- 🌏 **多語言架構重構**: 支援動態載入
- 📁 **語言檔案模組化**: 模組化組織語言檔案
---
## [v2.0.3] - 編碼問題修復
### 🐛 重要修復
- 🛡️ **完全修復中文字符編碼問題**: 解決所有中文顯示相關問題
- 🔧 **解決 JSON 解析錯誤**: 修復資料解析錯誤
---
## [v2.0.0] - Web UI 支援
### 🌟 重大功能
-**新增 Web UI 支援**: 支援遠端環境使用
-**自動環境檢測**: 自動選擇合適的界面
-**WebSocket 即時通訊**: 實現即時雙向通訊
---
## 圖例說明
| 圖示 | 意義 |
|------|------|
| 🌟 | 版本亮點 |
| ✨ | 新功能 |
| 🚀 | 改進功能 |
| 🐛 | 問題修復 |
| 🔄 | 重構變更 |
| 🎨 | 界面優化 |
| ⚙️ | 設定相關 |
| 🖥️ | 視窗相關 |
| 🌐 | 多語言/網路相關 |
| 📁 | 檔案結構 |
| ⌨️ | 快捷鍵 |
| 🖼️ | 圖片功能 |
---
**完整專案資訊:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced)

66
RELEASE_NOTES/README.md Normal file
View File

@@ -0,0 +1,66 @@
# 發佈說明管理系統
此目錄包含了所有版本的結構化發佈說明,支援多語言,採用**雙重架構**來兼顧統一的更新日誌檢視和自動化發佈功能。
## 📁 資料夾結構
```
RELEASE_NOTES/
├── README.md # 此說明文檔
├── template.md # 發佈說明模板
├── CHANGELOG.en.md # 完整更新歷史(英文)
├── CHANGELOG.zh-TW.md # 完整更新歷史(繁體中文)
├── CHANGELOG.zh-CN.md # 完整更新歷史(簡體中文)
├── v2.2.1/ # 版本特定資料夾(供 GitHub Actions 使用)
│ ├── en.md # 英文發佈說明
│ ├── zh-TW.md # 繁體中文發佈說明
│ └── zh-CN.md # 簡體中文發佈說明
├── v2.2.0/
│ ├── en.md
│ ├── zh-TW.md
│ └── zh-CN.md
└── ... # 其他版本
```
## 🔄 雙重架構優勢
### 📖 CHANGELOG 檔案
- **完整歷史檢視**: 用戶可在單一檔案查看所有版本更新
- **便於搜尋**: 快速找到特定功能的引入版本
- **GitHub 顯示友好**: 更新歷史在專案主頁直接可見
### 📁 版本資料夾
- **自動化友好**: GitHub Actions 可精確提取特定版本內容
- **維護簡便**: 新版本只需建立新資料夾,無需修改大檔案
- **工具整合**: 各種自動化工具容易解析單一版本檔案
## 🤖 GitHub Actions 整合
GitHub Actions 工作流程會自動執行:
1. **版本檢測**: 從 git 標籤檢測要發佈的版本
2. **發佈說明提取**: 從 `RELEASE_NOTES/vX.Y.Z/` 提取對應的發佈說明
3. **多語言組合**: 將所有語言版本組合成結構化的發佈內容
4. **GitHub Release 發佈**: 將格式化的發佈說明發佈到 GitHub Releases
5. **CHANGELOG 同步**: 自動將新版本內容更新到統一的 CHANGELOG 檔案
## 📝 撰寫發佈說明
### 新版本發佈:
1. **建立版本資料夾**: 為每個版本建立新資料夾(例如 `v2.2.1/`
2. **添加語言檔案**: 為每種語言添加發佈說明(`en.md``zh-TW.md``zh-CN.md`
3. **遵循模板**: 依照 `template.md` 中的模板結構
4. **更新 CHANGELOG**: 將新版本內容添加到各個 CHANGELOG 檔案的頂部
5. **一致格式**: 在各語言中使用一致的表情符號和格式
### 維護作業:
- **版本資料夾**: 供 GitHub Actions 自動化發佈使用
- **CHANGELOG 檔案**: 為用戶和文檔提供統一檢視
- **模板**: 參考 `template.md` 確保結構一致
## 🔧 格式指南
- **表情符號一致性**: 為不同類型的變更使用一致的表情符號前綴
- **簡潔描述**: 保持項目符號簡潔但具描述性
- **問題引用**: 在適當的地方包含問題引用(例如 `fixes #10`
- **平行結構**: 在所有語言中保持平行結構
- **時間順序**: 在 CHANGELOG 檔案中將最新版本放在頂部

24
RELEASE_NOTES/template.md Normal file
View File

@@ -0,0 +1,24 @@
# Release vX.Y.Z - [發佈標題]
## 🌟 亮點
本次發佈主要變更的簡要摘要。
## ✨ 新功能
- 🆕 **功能名稱**: 新功能的描述
- 🎨 **界面增強**: 介面改進的描述
## 🐛 錯誤修復
- 🔧 **問題修復**: 修復內容的描述 (fixes #issue_number)
- 🛡️ **安全修復**: 安全相關的改進
## 🚀 改進功能
-**效能優化**: 效能最佳化
- 📱 **用戶體驗增強**: 用戶體驗改進
## 🔄 變更
- 🔀 **重大變更**: 任何重大變更(僅限主要版本)
- 📝 **文檔更新**: 文檔更新
---
**說明**: 此模板應該適應每種語言CHANGELOG.en.md, CHANGELOG.zh-TW.md, CHANGELOG.zh-CN.md
**注意**: 版本發佈文件不包含安裝與相關連結部分,這些內容已移至各語言的完整 CHANGELOG 文件中

View File

@@ -0,0 +1,28 @@
# 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

@@ -0,0 +1,28 @@
# 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

@@ -0,0 +1,28 @@
# 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

@@ -0,0 +1,30 @@
# 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

@@ -0,0 +1,30 @@
# 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

@@ -0,0 +1,30 @@
# 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

@@ -0,0 +1,42 @@
# 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

@@ -0,0 +1,42 @@
# 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

@@ -0,0 +1,42 @@
# 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

@@ -0,0 +1,23 @@
# 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

@@ -0,0 +1,23 @@
# 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

@@ -0,0 +1,23 @@
# 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

@@ -0,0 +1,28 @@
# 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

@@ -0,0 +1,28 @@
# 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

@@ -0,0 +1,28 @@
# 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)

184
debug_websocket.html Normal file
View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket 診斷工具</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #1a1a1a;
color: #ffffff;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
.success { background: #2d5a2d; }
.error { background: #5a2d2d; }
.info { background: #2d4a5a; }
.warning { background: #5a5a2d; }
.log {
background: #2a2a2a;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
font-family: monospace;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
button {
background: #4a90e2;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #357abd;
}
input {
padding: 8px;
margin: 5px;
border: 1px solid #555;
background: #333;
color: white;
border-radius: 3px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔧 WebSocket 診斷工具</h1>
<div id="status" class="status info">
準備開始診斷...
</div>
<div>
<label>WebSocket URL:</label>
<input type="text" id="wsUrl" value="ws://127.0.0.1:8767/ws" style="width: 300px;">
<button onclick="testConnection()">🔗 測試連接</button>
<button onclick="clearLog()">🗑️ 清除日誌</button>
</div>
<div>
<label>發送消息:</label>
<input type="text" id="messageInput" placeholder='{"type": "get_status"}' style="width: 300px;">
<button onclick="sendMessage()">📤 發送</button>
</div>
<div id="log" class="log">等待操作...</div>
</div>
<script>
let websocket = null;
let logElement = document.getElementById('log');
let statusElement = document.getElementById('status');
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
logElement.textContent += `[${timestamp}] ${message}\n`;
logElement.scrollTop = logElement.scrollHeight;
// 更新狀態
statusElement.textContent = message;
statusElement.className = `status ${type}`;
}
function clearLog() {
logElement.textContent = '';
log('日誌已清除');
}
function testConnection() {
const url = document.getElementById('wsUrl').value;
if (websocket) {
log('關閉現有連接...', 'warning');
websocket.close();
websocket = null;
}
log(`嘗試連接到: ${url}`, 'info');
try {
websocket = new WebSocket(url);
websocket.onopen = function(event) {
log('✅ WebSocket 連接成功!', 'success');
};
websocket.onmessage = function(event) {
log(`📨 收到消息: ${event.data}`, 'success');
try {
const data = JSON.parse(event.data);
log(`📋 解析後的數據: ${JSON.stringify(data, null, 2)}`, 'info');
} catch (e) {
log(`⚠️ JSON 解析失敗: ${e.message}`, 'warning');
}
};
websocket.onclose = function(event) {
log(`🔌 連接已關閉 - Code: ${event.code}, Reason: ${event.reason}`, 'warning');
websocket = null;
};
websocket.onerror = function(error) {
log(`❌ WebSocket 錯誤: ${error}`, 'error');
console.error('WebSocket error:', error);
};
} catch (error) {
log(`❌ 連接失敗: ${error.message}`, 'error');
}
}
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (!message) {
log('⚠️ 請輸入要發送的消息', 'warning');
return;
}
if (!websocket || websocket.readyState !== WebSocket.OPEN) {
log('❌ WebSocket 未連接', 'error');
return;
}
try {
websocket.send(message);
log(`📤 已發送: ${message}`, 'info');
messageInput.value = '';
} catch (error) {
log(`❌ 發送失敗: ${error.message}`, 'error');
}
}
// 頁面加載時自動測試
window.onload = function() {
log('🚀 WebSocket 診斷工具已載入');
log('💡 點擊 "測試連接" 開始診斷');
};
// Enter 鍵發送消息
document.getElementById('messageInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>

142
docs/en/cache-management.md Normal file
View File

@@ -0,0 +1,142 @@
# UV Cache Management Guide
## 🔍 Problem Description
Since this project uses `uvx` for execution, cache files are created in the system with each run. Over time, these caches can consume significant disk space.
### Cache Location
- **Windows**: `%USERPROFILE%\AppData\Local\uv\cache`
- **macOS/Linux**: `~/.cache/uv`
## 🧹 Cleanup Methods
### Method 1: Using UV Built-in Commands (Recommended)
```bash
# Check cache location
uv cache dir
# Clean all cache
uv cache clean
```
### Method 2: Using Project-Provided Cleanup Tool
```bash
# Check cache size
python scripts/cleanup_cache.py --size
# Preview cleanup content
python scripts/cleanup_cache.py --dry-run
# Execute cleanup
python scripts/cleanup_cache.py --clean
# Force cleanup (attempts to close related processes)
python scripts/cleanup_cache.py --force
```
## ⚠️ Common Issues
### Issue: "File is being used by another process" error during cleanup
**Cause**: MCP server or other uvx processes are running
**Solutions**:
1. **Close related processes**:
- Close Claude Desktop or other MCP-using applications
- Terminate all `uvx` related processes
2. **Use force cleanup**:
```bash
python scripts/cleanup_cache.py --force
```
3. **Manual cleanup**:
```bash
# Windows
taskkill /f /im uvx.exe
taskkill /f /im python.exe /fi "WINDOWTITLE eq *mcp-feedback-enhanced*"
# Then execute cleanup
uv cache clean
```
### Issue: Cache grows large again quickly after cleanup
**Cause**: Frequent use of `uvx mcp-feedback-enhanced@latest`
**Recommendations**:
1. **Regular cleanup**: Recommend weekly or monthly cleanup
2. **Monitor size**: Regularly check cache size
3. **Consider local installation**: For developers, consider local installation instead of uvx
## 📊 Cache Size Monitoring
### Check Cache Size
```bash
# Using cleanup tool
python scripts/cleanup_cache.py --size
# Or check directory size directly (Windows)
dir "%USERPROFILE%\AppData\Local\uv\cache" /s
# macOS/Linux
du -sh ~/.cache/uv
```
### Recommended Cleanup Frequency
| Cache Size | Recommended Action |
|-----------|-------------------|
| < 100MB | No cleanup needed |
| 100MB-500MB | Consider cleanup |
| > 500MB | Cleanup recommended |
| > 1GB | Cleanup strongly recommended |
## 🔧 Automated Cleanup
### Windows Scheduled Task
```batch
@echo off
cd /d "G:\github\interactive-feedback-mcp"
python scripts/cleanup_cache.py --clean
```
### macOS/Linux Cron Job
```bash
# Weekly cleanup on Sunday
0 2 * * 0 cd /path/to/interactive-feedback-mcp && python scripts/cleanup_cache.py --clean
```
## 💡 Best Practices
1. **Regular monitoring**: Check cache size monthly
2. **Timely cleanup**: Clean when cache exceeds 500MB
3. **Close processes**: Ensure related MCP services are closed before cleanup
4. **Backup important data**: Ensure important projects are backed up before cleanup
## 🆘 Troubleshooting
### Common Causes of Cleanup Failure
1. **Process occupation**: MCP server is running
2. **Insufficient permissions**: Administrator privileges required
3. **Disk errors**: File system errors
### Resolution Steps
1. Close all MCP-related processes
2. Run cleanup command as administrator
3. If still failing, restart computer and try again
4. Consider manually deleting parts of cache directory
## 📞 Support
If you encounter cleanup issues, please:
1. Check the troubleshooting section in this document
2. Report issues on [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues)
3. Provide error messages and system information

BIN
docs/en/images/gui1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/en/images/gui2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/en/images/web1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
docs/en/images/web2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -0,0 +1,142 @@
# UV Cache 管理指南
## 🔍 问题说明
由于本项目使用 `uvx` 执行,每次运行都会在系统中建立 cache 文件。随着时间推移,这些 cache 可能会占用大量磁盘空间。
### Cache 位置
- **Windows**: `%USERPROFILE%\AppData\Local\uv\cache`
- **macOS/Linux**: `~/.cache/uv`
## 🧹 清理方法
### 方法一:使用 UV 内建命令(推荐)
```bash
# 查看 cache 位置
uv cache dir
# 清理所有 cache
uv cache clean
```
### 方法二:使用项目提供的清理工具
```bash
# 查看 cache 大小
python scripts/cleanup_cache.py --size
# 预览清理内容
python scripts/cleanup_cache.py --dry-run
# 执行清理
python scripts/cleanup_cache.py --clean
# 强制清理(会尝试关闭相关程序)
python scripts/cleanup_cache.py --force
```
## ⚠️ 常见问题
### 问题:清理时出现「文件正由另一个程序使用」错误
**原因**:有 MCP 服务器或其他 uvx 程序正在运行
**解决方案**
1. **关闭相关程序**
- 关闭 Claude Desktop 或其他使用 MCP 的应用
- 结束所有 `uvx` 相关程序
2. **使用强制清理**
```bash
python scripts/cleanup_cache.py --force
```
3. **手动清理**
```bash
# Windows
taskkill /f /im uvx.exe
taskkill /f /im python.exe /fi "WINDOWTITLE eq *mcp-feedback-enhanced*"
# 然后执行清理
uv cache clean
```
### 问题:清理后 cache 很快又变大
**原因**:频繁使用 `uvx mcp-feedback-enhanced@latest`
**建议**
1. **定期清理**:建议每周或每月清理一次
2. **监控大小**:定期检查 cache 大小
3. **考虑本地安装**:如果是开发者,可考虑本地安装而非每次使用 uvx
## 📊 Cache 大小监控
### 检查 Cache 大小
```bash
# 使用清理工具
python scripts/cleanup_cache.py --size
# 或直接查看目录大小Windows
dir "%USERPROFILE%\AppData\Local\uv\cache" /s
# macOS/Linux
du -sh ~/.cache/uv
```
### 建议的清理频率
| Cache 大小 | 建议动作 |
|-----------|---------|
| < 100MB | 无需清理 |
| 100MB-500MB | 可考虑清理 |
| > 500MB | 建议清理 |
| > 1GB | 强烈建议清理 |
## 🔧 自动化清理
### Windows 计划任务
```batch
@echo off
cd /d "G:\github\interactive-feedback-mcp"
python scripts/cleanup_cache.py --clean
```
### macOS/Linux Cron Job
```bash
# 每周日清理一次
0 2 * * 0 cd /path/to/interactive-feedback-mcp && python scripts/cleanup_cache.py --clean
```
## 💡 最佳实践
1. **定期监控**:每月检查一次 cache 大小
2. **适时清理**:当 cache 超过 500MB 时进行清理
3. **关闭程序**:清理前确保关闭相关 MCP 服务
4. **备份重要资料**:清理前确保重要项目已备份
## 🆘 故障排除
### 清理失败的常见原因
1. **程序占用**MCP 服务器正在运行
2. **权限不足**:需要管理员权限
3. **磁盘错误**:文件系统错误
### 解决步骤
1. 关闭所有 MCP 相关程序
2. 以管理员身份运行清理命令
3. 如果仍然失败,重启电脑后再试
4. 考虑手动删除部分 cache 目录
## 📞 支持
如果遇到清理问题,请:
1. 查看本文档的故障排除部分
2. 在 [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) 报告问题
3. 提供错误信息和系统信息

BIN
docs/zh-CN/images/gui1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/zh-CN/images/gui2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
docs/zh-CN/images/web1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
docs/zh-CN/images/web2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -0,0 +1,142 @@
# UV Cache 管理指南
## 🔍 問題說明
由於本專案使用 `uvx` 執行,每次運行都會在系統中建立 cache 檔案。隨著時間推移,這些 cache 可能會佔用大量磁碟空間。
### Cache 位置
- **Windows**: `%USERPROFILE%\AppData\Local\uv\cache`
- **macOS/Linux**: `~/.cache/uv`
## 🧹 清理方法
### 方法一:使用 UV 內建命令(推薦)
```bash
# 查看 cache 位置
uv cache dir
# 清理所有 cache
uv cache clean
```
### 方法二:使用專案提供的清理工具
```bash
# 查看 cache 大小
python scripts/cleanup_cache.py --size
# 預覽清理內容
python scripts/cleanup_cache.py --dry-run
# 執行清理
python scripts/cleanup_cache.py --clean
# 強制清理(會嘗試關閉相關程序)
python scripts/cleanup_cache.py --force
```
## ⚠️ 常見問題
### 問題:清理時出現「檔案正由另一個程序使用」錯誤
**原因**:有 MCP 服務器或其他 uvx 程序正在運行
**解決方案**
1. **關閉相關程序**
- 關閉 Claude Desktop 或其他使用 MCP 的應用
- 結束所有 `uvx` 相關程序
2. **使用強制清理**
```bash
python scripts/cleanup_cache.py --force
```
3. **手動清理**
```bash
# Windows
taskkill /f /im uvx.exe
taskkill /f /im python.exe /fi "WINDOWTITLE eq *mcp-feedback-enhanced*"
# 然後執行清理
uv cache clean
```
### 問題:清理後 cache 很快又變大
**原因**:頻繁使用 `uvx mcp-feedback-enhanced@latest`
**建議**
1. **定期清理**:建議每週或每月清理一次
2. **監控大小**:定期檢查 cache 大小
3. **考慮本地安裝**:如果是開發者,可考慮本地安裝而非每次使用 uvx
## 📊 Cache 大小監控
### 檢查 Cache 大小
```bash
# 使用清理工具
python scripts/cleanup_cache.py --size
# 或直接查看目錄大小Windows
dir "%USERPROFILE%\AppData\Local\uv\cache" /s
# macOS/Linux
du -sh ~/.cache/uv
```
### 建議的清理頻率
| Cache 大小 | 建議動作 |
|-----------|---------|
| < 100MB | 無需清理 |
| 100MB-500MB | 可考慮清理 |
| > 500MB | 建議清理 |
| > 1GB | 強烈建議清理 |
## 🔧 自動化清理
### Windows 排程任務
```batch
@echo off
cd /d "G:\github\interactive-feedback-mcp"
python scripts/cleanup_cache.py --clean
```
### macOS/Linux Cron Job
```bash
# 每週日清理一次
0 2 * * 0 cd /path/to/interactive-feedback-mcp && python scripts/cleanup_cache.py --clean
```
## 💡 最佳實踐
1. **定期監控**:每月檢查一次 cache 大小
2. **適時清理**:當 cache 超過 500MB 時進行清理
3. **關閉程序**:清理前確保關閉相關 MCP 服務
4. **備份重要資料**:清理前確保重要專案已備份
## 🆘 故障排除
### 清理失敗的常見原因
1. **程序佔用**MCP 服務器正在運行
2. **權限不足**:需要管理員權限
3. **磁碟錯誤**:檔案系統錯誤
### 解決步驟
1. 關閉所有 MCP 相關程序
2. 以管理員身份運行清理命令
3. 如果仍然失敗,重啟電腦後再試
4. 考慮手動刪除部分 cache 目錄
## 📞 支援
如果遇到清理問題,請:
1. 查看本文檔的故障排除部分
2. 在 [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues) 回報問題
3. 提供錯誤訊息和系統資訊

BIN
docs/zh-TW/images/gui1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/zh-TW/images/gui2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
docs/zh-TW/images/web1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
docs/zh-TW/images/web2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

60
pyproject.toml Normal file
View File

@@ -0,0 +1,60 @@
[project]
name = "mcp-tingquan"
version = "0.0.1"
description = "MCP Feedback TingQuan Enhanced - Interactive user feedback and command execution for AI-assisted development"
readme = "README.md"
requires-python = ">=3.11"
authors = [
{ name = "maticarmy", email = "maticarmy@example.com" }
]
keywords = ["mcp", "ai", "feedback", "gui", "web-ui", "interactive", "development"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: User Interfaces",
]
dependencies = [
"fastmcp>=2.0.0",
"psutil>=7.0.0",
"pyside6>=6.8.2.1",
"fastapi>=0.115.0",
"uvicorn>=0.30.0",
"jinja2>=3.1.0",
"websockets>=13.0.0",
"aiohttp>=3.8.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
]
[project.urls]
Homepage = "https://github.com/maticarmy/mcp-tingquan"
Repository = "https://github.com/maticarmy/mcp-tingquan"
Issues = "https://github.com/maticarmy/mcp-tingquan/issues"
[project.scripts]
mcp-tingquan = "mcp_feedback_enhanced.__main__:main"
interactive-tingquan-mcp = "mcp_feedback_enhanced.__main__:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_feedback_enhanced"]
[tool.uv]
dev-dependencies = [
"bump2version>=1.0.1",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"twine>=6.1.0",
]

115
release_body.md Normal file
View File

@@ -0,0 +1,115 @@
## 🌐 Multi-Language Release Notes
### 🇺🇸 English
# 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)
---
### 🇹🇼 繁體中文
# 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)
---
### 🇨🇳 简体中文
# 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)
---
## 📦 Installation & Update
```bash
# Quick test latest version
uvx mcp-feedback-enhanced@latest test
# Update to this specific version
uvx mcp-feedback-enhanced@v2.2.5 test
```
## 🔗 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** 🤖

284
scripts/cleanup_cache.py Normal file
View File

@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""
UV Cache 清理腳本
================
定期清理 uv cache 以防止磁碟空間不斷增加
特別針對 Windows 系統「檔案正由另一個程序使用」的問題提供解決方案
使用方式:
python scripts/cleanup_cache.py --size # 查看 cache 大小和詳細資訊
python scripts/cleanup_cache.py --dry-run # 預覽將要清理的內容(不實際清理)
python scripts/cleanup_cache.py --clean # 執行標準清理
python scripts/cleanup_cache.py --force # 強制清理(會嘗試關閉相關程序)
功能特色:
- 智能跳過正在使用中的檔案
- 提供強制清理模式
- 詳細的清理統計和進度顯示
- 支援 Windows/macOS/Linux 跨平台
"""
import subprocess
import sys
import argparse
import shutil
from pathlib import Path
import os
def get_cache_dir():
"""取得 uv cache 目錄"""
# Windows 預設路徑
if os.name == 'nt':
return Path.home() / "AppData" / "Local" / "uv"
# macOS/Linux 預設路徑
else:
return Path.home() / ".cache" / "uv"
def get_cache_size(cache_dir):
"""計算 cache 目錄大小"""
if not cache_dir.exists():
return 0
total_size = 0
for dirpath, dirnames, filenames in os.walk(cache_dir):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
try:
total_size += os.path.getsize(filepath)
except (OSError, FileNotFoundError):
pass
return total_size
def format_size(size_bytes):
"""格式化檔案大小顯示"""
if size_bytes == 0:
return "0 B"
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.1f} TB"
def run_uv_command(command, check=True):
"""執行 uv 命令"""
try:
result = subprocess.run(
["uv"] + command,
capture_output=True,
text=True,
check=check
)
return result
except subprocess.CalledProcessError as e:
print(f"❌ 命令執行失敗: uv {' '.join(command)}")
print(f"錯誤: {e.stderr}")
return None
except FileNotFoundError:
print("❌ 找不到 uv 命令,請確認 uv 已正確安裝")
return None
def show_cache_info():
"""顯示 cache 資訊"""
print("🔍 UV Cache 資訊")
print("=" * 50)
cache_dir = get_cache_dir()
print(f"Cache 目錄: {cache_dir}")
if cache_dir.exists():
cache_size = get_cache_size(cache_dir)
print(f"Cache 大小: {format_size(cache_size)}")
# 顯示子目錄大小
subdirs = []
for subdir in cache_dir.iterdir():
if subdir.is_dir():
subdir_size = get_cache_size(subdir)
subdirs.append((subdir.name, subdir_size))
if subdirs:
print("\n📁 子目錄大小:")
subdirs.sort(key=lambda x: x[1], reverse=True)
for name, size in subdirs[:10]: # 顯示前10大
print(f" {name}: {format_size(size)}")
else:
print("Cache 目錄不存在")
def clean_cache_selective(cache_dir, dry_run=False):
"""選擇性清理 cache跳過正在使用的檔案"""
cleaned_count = 0
skipped_count = 0
total_saved = 0
print(f"🔍 掃描 cache 目錄: {cache_dir}")
# 遍歷 cache 目錄
for root, dirs, files in os.walk(cache_dir):
# 跳過一些可能正在使用的目錄
if any(skip_dir in root for skip_dir in ['Scripts', 'Lib', 'pyvenv.cfg']):
continue
for file in files:
file_path = Path(root) / file
try:
if dry_run:
file_size = file_path.stat().st_size
total_saved += file_size
cleaned_count += 1
if cleaned_count <= 10: # 只顯示前10個
print(f" 將清理: {file_path.relative_to(cache_dir)} ({format_size(file_size)})")
else:
file_size = file_path.stat().st_size
file_path.unlink()
total_saved += file_size
cleaned_count += 1
except (OSError, PermissionError, FileNotFoundError) as e:
skipped_count += 1
if not dry_run and skipped_count <= 5: # 只顯示前5個錯誤
print(f" ⚠️ 跳過: {file_path.name} (正在使用中)")
return cleaned_count, skipped_count, total_saved
def clean_cache(dry_run=False):
"""清理 cache"""
action = "預覽" if dry_run else "執行"
print(f"🧹 {action} UV Cache 清理")
print("=" * 50)
# 顯示清理前的大小
cache_dir = get_cache_dir()
if cache_dir.exists():
before_size = get_cache_size(cache_dir)
print(f"清理前大小: {format_size(before_size)}")
else:
print("Cache 目錄不存在,無需清理")
return
if dry_run:
print("\n🔍 將要清理的內容:")
# 先嘗試 uv cache clean --dry-run
result = run_uv_command(["cache", "clean", "--dry-run"], check=False)
if result and result.returncode == 0:
print(result.stdout)
else:
print(" 使用自定義掃描...")
cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=True)
print(f"\n📊 預覽結果:")
print(f" 可清理檔案: {cleaned_count}")
print(f" 預計節省: {format_size(total_saved)}")
else:
print("\n🗑️ 正在清理...")
# 先嘗試標準清理
result = run_uv_command(["cache", "clean"], check=False)
if result and result.returncode == 0:
print("✅ 標準 Cache 清理完成")
else:
print("⚠️ 標準清理失敗,使用選擇性清理...")
cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=False)
print(f"\n📊 清理結果:")
print(f" 已清理檔案: {cleaned_count}")
print(f" 跳過檔案: {skipped_count}")
print(f" 節省空間: {format_size(total_saved)}")
if skipped_count > 0:
print(f"\n💡 提示: {skipped_count} 個檔案正在使用中,已跳過")
print(" 建議關閉相關程序後重新執行清理")
# 顯示清理後的大小
if cache_dir.exists():
after_size = get_cache_size(cache_dir)
saved_size = before_size - after_size
print(f"\n📈 總體效果:")
print(f" 清理前: {format_size(before_size)}")
print(f" 清理後: {format_size(after_size)}")
print(f" 實際節省: {format_size(saved_size)}")
else:
print(f" 節省空間: {format_size(before_size)}")
def force_clean_cache():
"""強制清理 cache關閉相關程序後"""
print("🔥 強制清理模式")
print("=" * 50)
print("⚠️ 警告:此模式會嘗試關閉可能使用 cache 的程序")
confirm = input("確定要繼續嗎?(y/N): ")
if confirm.lower() != 'y':
print("❌ 已取消")
return
cache_dir = get_cache_dir()
if not cache_dir.exists():
print("Cache 目錄不存在")
return
before_size = get_cache_size(cache_dir)
print(f"清理前大小: {format_size(before_size)}")
# 嘗試關閉可能的 uvx 程序
print("\n🔍 檢查相關程序...")
try:
import psutil
killed_processes = []
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
if proc.info['name'] and any(name in proc.info['name'].lower()
for name in ['uvx', 'uv.exe', 'python.exe']):
cmdline = ' '.join(proc.info['cmdline'] or [])
if 'mcp-feedback-enhanced' in cmdline or 'uvx' in cmdline:
print(f" 終止程序: {proc.info['name']} (PID: {proc.info['pid']})")
proc.terminate()
killed_processes.append(proc.info['pid'])
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
if killed_processes:
print(f" 已終止 {len(killed_processes)} 個程序")
import time
time.sleep(2) # 等待程序完全關閉
else:
print(" 未發現相關程序")
except ImportError:
print(" 無法檢查程序(需要 psutil繼續清理...")
# 再次嘗試標準清理
print("\n🗑️ 執行清理...")
result = run_uv_command(["cache", "clean"], check=False)
if result and result.returncode == 0:
print("✅ 強制清理成功")
else:
print("⚠️ 標準清理仍然失敗,使用檔案級清理...")
cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=False)
print(f" 清理檔案: {cleaned_count}, 跳過: {skipped_count}")
# 顯示結果
after_size = get_cache_size(cache_dir)
saved_size = before_size - after_size
print(f"\n📈 清理結果:")
print(f" 節省空間: {format_size(saved_size)}")
def main():
parser = argparse.ArgumentParser(description="UV Cache 清理工具")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--size", action="store_true", help="顯示 cache 大小資訊")
group.add_argument("--dry-run", action="store_true", help="預覽清理內容(不實際清理)")
group.add_argument("--clean", action="store_true", help="執行 cache 清理")
group.add_argument("--force", action="store_true", help="強制清理(會嘗試關閉相關程序)")
args = parser.parse_args()
if args.size:
show_cache_info()
elif args.dry_run:
clean_cache(dry_run=True)
elif args.clean:
clean_cache(dry_run=False)
elif args.force:
force_clean_cache()
if __name__ == "__main__":
main()

102
scripts/release.py Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
本地發布腳本
用法:
python scripts/release.py patch # 2.0.0 -> 2.0.1
python scripts/release.py minor # 2.0.0 -> 2.1.0
python scripts/release.py major # 2.0.0 -> 3.0.0
"""
import subprocess
import sys
import re
from pathlib import Path
def run_cmd(cmd, check=True):
"""執行命令並返回結果"""
print(f"🔨 執行: {cmd}")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if check and result.returncode != 0:
print(f"❌ 錯誤: {result.stderr}")
sys.exit(1)
return result
def get_current_version():
"""從 pyproject.toml 獲取當前版本"""
pyproject_path = Path("pyproject.toml")
content = pyproject_path.read_text(encoding="utf-8")
match = re.search(r'version = "([^"]+)"', content)
if match:
return match.group(1)
raise ValueError("無法找到版本號")
def bump_version(version_type):
"""更新版本號"""
if version_type not in ['patch', 'minor', 'major']:
print("❌ 版本類型必須是: patch, minor, major")
sys.exit(1)
current = get_current_version()
print(f"📦 當前版本: {current}")
# 使用 bump2version with allow-dirty
run_cmd(f"uv run bump2version --allow-dirty {version_type}")
new_version = get_current_version()
print(f"🎉 新版本: {new_version}")
return current, new_version
def main():
if len(sys.argv) != 2:
print(__doc__)
sys.exit(1)
version_type = sys.argv[1]
print("🚀 開始發布流程...")
# 檢查 Git 狀態(僅提示,不阻止)
result = run_cmd("git status --porcelain", check=False)
if result.stdout.strip():
print("⚠️ 有未提交的變更:")
print(result.stdout)
print("💡 將繼續執行(使用 --allow-dirty 模式)")
# 更新版本
old_version, new_version = bump_version(version_type)
# 建置套件
print("📦 建置套件...")
run_cmd("uv build")
# 檢查套件
print("🔍 檢查套件...")
run_cmd("uv run twine check dist/*")
# 提交所有變更(包括版本更新)
print("💾 提交版本更新...")
run_cmd("git add .")
run_cmd(f'git commit -m "🔖 Release v{new_version}"')
run_cmd(f'git tag "v{new_version}"')
# 詢問是否發布
print(f"\n✅ 準備發布版本 {old_version} -> {new_version}")
choice = input("是否發布到 PyPI (y/N): ")
if choice.lower() == 'y':
print("🚀 發布到 PyPI...")
run_cmd("uv run twine upload dist/*")
print("📤 推送到 GitHub...")
run_cmd("git push origin main")
run_cmd(f'git push origin "v{new_version}"')
print(f"🎉 發布完成!版本 v{new_version} 已上線")
print(f"📦 安裝命令: uvx mcp-feedback-enhanced")
else:
print("⏸️ 發布已取消,版本已更新但未發布")
print("💡 您可以稍後手動發布: uv run twine upload dist/*")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP Feedback TingQuan Enhanced
===============================
互動式用戶回饋 MCP 伺服器,提供 AI 輔助開發中的回饋收集功能。
作者: maticarmy
原始作者: Fábio Ferreira
增強功能: Web UI 支援、圖片上傳、現代化界面設計
特色:
- 雙介面支援Qt GUI 和 Web UI
- 智慧環境檢測
- 命令執行功能
- 圖片上傳支援
- 現代化深色主題
- 重構的模組化架構
"""
__version__ = "0.0.1"
__author__ = "maticarmy"
__email__ = "maticarmy@example.com"
import os
from .server import main as run_server
# 導入新的 Web UI 模組
from .web import WebUIManager, launch_web_feedback_ui, get_web_ui_manager, stop_web_ui
# 條件性導入 GUI 模組(只有在不強制使用 Web 時才導入)
feedback_ui = None
if not os.getenv('FORCE_WEB', '').lower() in ('true', '1', 'yes'):
try:
from .gui import feedback_ui
except ImportError:
# 如果 GUI 依賴不可用,設為 None
feedback_ui = None
# 主要導出介面
__all__ = [
"run_server",
"feedback_ui",
"WebUIManager",
"launch_web_feedback_ui",
"get_web_ui_manager",
"stop_web_ui",
"__version__",
"__author__",
]
def main():
"""主要入口點,用於 uvx 執行"""
from .__main__ import main as cli_main
return cli_main()

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP Interactive Feedback Enhanced - 主程式入口
==============================================
此檔案允許套件透過 `python -m mcp_feedback_enhanced` 執行。
使用方法:
python -m mcp_feedback_enhanced # 啟動 MCP 伺服器
python -m mcp_feedback_enhanced test # 執行測試
"""
import sys
import argparse
import os
def main():
"""主程式入口點"""
parser = argparse.ArgumentParser(
description="MCP Feedback TingQuan Enhanced - 互動式回饋收集 MCP 伺服器"
)
subparsers = parser.add_subparsers(dest='command', help='可用命令')
# 伺服器命令(預設)
server_parser = subparsers.add_parser('server', help='啟動 MCP 伺服器(預設)')
# 測試命令
test_parser = subparsers.add_parser('test', help='執行測試')
test_parser.add_argument('--web', action='store_true', help='測試 Web UI (自動持續運行)')
test_parser.add_argument('--gui', action='store_true', help='測試 Qt GUI (快速測試)')
test_parser.add_argument('--enhanced', action='store_true', help='執行增強 MCP 測試 (推薦)')
test_parser.add_argument('--scenario', help='運行特定的測試場景')
test_parser.add_argument('--tags', help='根據標籤運行測試場景 (逗號分隔)')
test_parser.add_argument('--list-scenarios', action='store_true', help='列出所有可用的測試場景')
test_parser.add_argument('--report-format', choices=['html', 'json', 'markdown'], help='報告格式')
test_parser.add_argument('--timeout', type=int, help='測試超時時間 (秒)')
# 版本命令
version_parser = subparsers.add_parser('version', help='顯示版本資訊')
args = parser.parse_args()
if args.command == 'test':
run_tests(args)
elif args.command == 'version':
show_version()
elif args.command == 'server':
run_server()
elif args.command is None:
run_server()
else:
# 不應該到達這裡
parser.print_help()
sys.exit(1)
def run_server():
"""啟動 MCP 伺服器"""
from .server import main as server_main
return server_main()
def run_tests(args):
"""執行測試"""
# 啟用調試模式以顯示測試過程
os.environ["MCP_DEBUG"] = "true"
if args.enhanced or args.scenario or args.tags or args.list_scenarios:
# 使用新的增強測試系統
print("🚀 執行增強 MCP 測試系統...")
import asyncio
from .test_mcp_enhanced import MCPTestRunner, TestConfig
# 創建配置
config = TestConfig.from_env()
if args.timeout:
config.test_timeout = args.timeout
if args.report_format:
config.report_format = args.report_format
runner = MCPTestRunner(config)
async def run_enhanced_tests():
try:
if args.list_scenarios:
# 列出測試場景
tags = args.tags.split(',') if args.tags else None
runner.list_scenarios(tags)
return True
success = False
if args.scenario:
# 運行特定場景
success = await runner.run_single_scenario(args.scenario)
elif args.tags:
# 根據標籤運行
tags = [tag.strip() for tag in args.tags.split(',')]
success = await runner.run_scenarios_by_tags(tags)
else:
# 運行所有場景
success = await runner.run_all_scenarios()
return success
except Exception as e:
print(f"❌ 增強測試執行失敗: {e}")
return False
success = asyncio.run(run_enhanced_tests())
if not success:
sys.exit(1)
elif args.web:
print("🧪 執行 Web UI 測試...")
from .test_web_ui import test_web_ui, interactive_demo
success, session_info = test_web_ui()
if not success:
sys.exit(1)
# Web UI 測試自動啟用持續模式
if session_info:
print("📝 Web UI 測試完成,進入持續模式...")
print("💡 提示:服務器將持續運行,可在瀏覽器中測試互動功能")
print("💡 按 Ctrl+C 停止服務器")
interactive_demo(session_info)
elif args.gui:
print("🧪 執行 Qt GUI 測試...")
from .test_qt_gui import test_qt_gui
if not test_qt_gui():
sys.exit(1)
else:
# 默認執行增強測試系統的快速測試
print("🧪 執行快速測試套件 (使用增強測試系統)...")
print("💡 提示:使用 --enhanced 參數可執行完整測試")
import asyncio
from .test_mcp_enhanced import MCPTestRunner, TestConfig
config = TestConfig.from_env()
config.test_timeout = 60 # 快速測試使用較短超時
runner = MCPTestRunner(config)
async def run_quick_tests():
try:
# 運行快速測試標籤
success = await runner.run_scenarios_by_tags(["quick"])
return success
except Exception as e:
print(f"❌ 快速測試執行失敗: {e}")
return False
success = asyncio.run(run_quick_tests())
if not success:
sys.exit(1)
print("🎉 快速測試通過!")
print("💡 使用 'test --enhanced' 執行完整測試套件")
def show_version():
"""顯示版本資訊"""
from . import __version__, __author__
print(f"MCP Feedback TingQuan Enhanced v{__version__}")
print(f"作者: {__author__}")
print(f"GitHub: https://github.com/maticarmy/mcp-tingquan")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
統一調試日誌模組
================
提供統一的調試日誌功能,確保調試輸出不會干擾 MCP 通信。
所有調試輸出都會發送到 stderr並且只在調試模式啟用時才輸出。
使用方法:
```python
from .debug import debug_log
debug_log("這是一條調試信息")
```
環境變數控制:
- MCP_DEBUG=true/1/yes/on: 啟用調試模式
- MCP_DEBUG=false/0/no/off: 關閉調試模式(默認)
作者: Minidoracat
"""
import os
import sys
from typing import Any
def debug_log(message: Any, prefix: str = "DEBUG") -> None:
"""
輸出調試訊息到標準錯誤,避免污染標準輸出
Args:
message: 要輸出的調試信息
prefix: 調試信息的前綴標識,默認為 "DEBUG"
"""
# 只在啟用調試模式時才輸出,避免干擾 MCP 通信
if not os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on"):
return
try:
# 確保消息是字符串類型
if not isinstance(message, str):
message = str(message)
# 安全地輸出到 stderr處理編碼問題
try:
print(f"[{prefix}] {message}", file=sys.stderr, flush=True)
except UnicodeEncodeError:
# 如果遇到編碼問題,使用 ASCII 安全模式
safe_message = message.encode('ascii', errors='replace').decode('ascii')
print(f"[{prefix}] {safe_message}", file=sys.stderr, flush=True)
except Exception:
# 最後的備用方案:靜默失敗,不影響主程序
pass
def gui_debug_log(message: Any) -> None:
"""GUI 模組專用的調試日誌"""
debug_log(message, "GUI")
def i18n_debug_log(message: Any) -> None:
"""國際化模組專用的調試日誌"""
debug_log(message, "I18N")
def server_debug_log(message: Any) -> None:
"""伺服器模組專用的調試日誌"""
debug_log(message, "SERVER")
def web_debug_log(message: Any) -> None:
"""Web UI 模組專用的調試日誌"""
debug_log(message, "WEB")
def is_debug_enabled() -> bool:
"""檢查是否啟用了調試模式"""
return os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on")
def set_debug_mode(enabled: bool) -> None:
"""設置調試模式(用於測試)"""
os.environ["MCP_DEBUG"] = "true" if enabled else "false"

View File

@@ -0,0 +1,25 @@
"""
互動式回饋收集 GUI 模組
=======================
基於 PySide6 的圖形用戶介面模組,提供直觀的回饋收集功能。
支援文字輸入、圖片上傳、命令執行等功能。
模組結構:
- main.py: 主要介面入口點
- window/: 窗口類別
- widgets/: 自定義元件
- styles/: 樣式定義
- utils/: 工具函數
- models/: 資料模型
作者: Fábio Ferreira
靈感來源: dotcursorrules.com
增強功能: 圖片支援和現代化界面設計
多語系支援: Minidoracat
重構: 模塊化設計
"""
from .main import feedback_ui, feedback_ui_with_timeout
__all__ = ['feedback_ui', 'feedback_ui_with_timeout']

View File

@@ -0,0 +1,172 @@
# 多語系檔案結構說明
## 📁 檔案結構
```
locales/
├── README.md # 此說明檔案
├── zh-TW/ # 繁體中文
│ └── translations.json
├── en/ # 英文
│ └── translations.json
└── zh-CN/ # 簡體中文
└── translations.json
```
## 🌐 翻譯檔案格式
每個語言的 `translations.json` 檔案都包含以下結構:
### 1. 元資料區塊 (meta)
```json
{
"meta": {
"language": "zh-TW",
"displayName": "繁體中文",
"author": "作者名稱",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
}
}
```
### 2. 應用程式區塊 (app)
```json
{
"app": {
"title": "應用程式標題",
"projectDirectory": "專案目錄",
"language": "語言",
"settings": "設定"
}
}
```
### 3. 分頁區塊 (tabs)
```json
{
"tabs": {
"feedback": "💬 回饋",
"command": "⚡ 命令",
"images": "🖼️ 圖片"
}
}
```
### 4. 其他功能區塊
- `feedback`: 回饋相關文字
- `command`: 命令執行相關文字
- `images`: 圖片上傳相關文字
- `buttons`: 按鈕文字
- `status`: 狀態訊息
- `aiSummary`: AI 摘要標題
- `languageSelector`: 語言選擇器標題
- `languageNames`: 語言顯示名稱
## 🔧 新增新語言步驟
### 1. 建立語言目錄
```bash
mkdir src/mcp_feedback_enhanced/locales/[語言代碼]
```
### 2. 複製翻譯檔案
```bash
cp src/mcp_feedback_enhanced/locales/en/translations.json \
src/mcp_feedback_enhanced/locales/[語言代碼]/translations.json
```
### 3. 修改元資料
```json
{
"meta": {
"language": "[語言代碼]",
"displayName": "[語言顯示名稱]",
"author": "[翻譯者姓名]",
"version": "1.0.0",
"lastUpdate": "[日期]"
}
}
```
### 4. 翻譯內容
逐一翻譯各個區塊的內容,保持 JSON 結構不變。
### 5. 註冊新語言
`i18n.py` 中將新語言代碼加入支援列表:
```python
self._supported_languages = ['zh-TW', 'en', 'zh-CN', '[新語言代碼]']
```
`i18n.js` 中也要加入:
```javascript
this.supportedLanguages = ['zh-TW', 'en', 'zh-CN', '[新語言代碼]'];
```
## 🎯 使用方式
### Python 後端
```python
from .i18n import t
# 新格式(建議)
title = t('app.title')
button_text = t('buttons.submitFeedback')
# 舊格式(兼容)
title = t('app_title')
button_text = t('btn_submit_feedback')
```
### JavaScript 前端
```javascript
// 新格式(建議)
const title = t('app.title');
const buttonText = t('buttons.submitFeedback');
// 舊格式(兼容)
const title = t('app_title');
const buttonText = t('btn_submit_feedback');
```
## 📋 翻譯檢查清單
建議在新增或修改翻譯時檢查:
- [ ] JSON 格式正確,沒有語法錯誤
- [ ] 所有必要的鍵值都存在
- [ ] 佔位符 `{param}` 格式正確
- [ ] 特殊字符和 Emoji 顯示正常
- [ ] 文字長度適合 UI 顯示
- [ ] 語言顯示名稱在 `languageNames` 中正確設定
## 🔄 向後兼容
新的多語系系統完全向後兼容舊的鍵值格式:
| 舊格式 | 新格式 |
|--------|--------|
| `app_title` | `app.title` |
| `btn_submit_feedback` | `buttons.submitFeedback` |
| `images_status` | `images.status` |
| `command_output` | `command.output` |
## 🚀 優勢特色
1. **結構化組織**:按功能區域分組,易於維護
2. **元資料支援**:包含版本、作者等資訊
3. **巢狀鍵值**:更清晰的命名空間
4. **動態載入**:前端支援從 API 載入翻譯
5. **向後兼容**:舊程式碼無需修改
6. **易於擴充**:新增語言非常簡單
## 📝 貢獻指南
歡迎貢獻新的語言翻譯:
1. Fork 專案
2. 按照上述步驟新增語言
3. 測試翻譯是否正確顯示
4. 提交 Pull Request
需要幫助可以參考現有的翻譯檔案作為範本。

View File

@@ -0,0 +1,207 @@
{
"meta": {
"language": "en",
"displayName": "English",
"author": "Minidoracat",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
},
"app": {
"title": "Interactive Feedback Collection",
"projectDirectory": "Project Directory",
"language": "Language",
"settings": "Settings",
"confirmCancel": "Confirm Cancel",
"confirmCancelMessage": "Are you sure you want to cancel feedback? All input content will be lost.",
"layoutChangeTitle": "Interface Layout Change",
"layoutChangeMessage": "Layout mode has been changed and requires reloading the interface to take effect.\nReload now?"
},
"tabs": {
"summary": "📋 AI Summary",
"feedback": "💬 Feedback",
"command": "⚡ Command",
"language": "⚙️ Settings",
"images": "🖼️ Images",
"about": " About"
},
"about": {
"appInfo": "Application Information",
"version": "Version",
"description": "A powerful MCP server that provides human-in-the-loop interactive feedback functionality for AI-assisted development tools. Supports dual interfaces (Qt GUI and Web UI) with rich features including image upload, command execution, and multi-language support.",
"projectLinks": "Project Links",
"githubProject": "GitHub Project",
"visitGithub": "Visit GitHub",
"contact": "Contact & Support",
"discordSupport": "Discord Support",
"joinDiscord": "Join Discord",
"contactDescription": "For technical support, issue reports, or feature suggestions, feel free to contact us through Discord community or GitHub Issues.",
"thanks": "Thanks & Contributions",
"thanksText": "Special thanks to the original author Fábio Ferreira (@fabiomlferreira) for creating the original interactive-feedback-mcp project.\n\nThis enhanced version is developed and maintained by Minidoracat, who has significantly expanded the project with GUI interface, image support, multi-language capabilities, and many other improvements.\n\nAlso thanks to sanshao85's mcp-feedback-collector project for UI design inspiration.\n\nOpen source collaboration makes technology better!"
},
"feedback": {
"title": "Your Feedback",
"description": "Please describe your thoughts, suggestions, or modifications needed for the AI's work.",
"placeholder": "Please enter your feedback, suggestions, or questions here...\n\n💡 Tips:\n• Press Ctrl+Enter (numpad supported) to submit quickly\n• Press Ctrl+V to paste images from clipboard",
"emptyTitle": "Feedback Content Empty",
"emptyMessage": "Please enter feedback content before submitting. You can describe your thoughts, suggestions, or areas that need modification.",
"outputPlaceholder": "Command output will appear here..."
},
"summary": {
"title": "AI Work Summary",
"description": "Below is the work content that AI has just completed for you. Please review and provide feedback.",
"testDescription": "Below is the message content replied by AI. Please review and provide feedback."
},
"command": {
"title": "Command Execution",
"description": "You can execute commands to verify results or gather more information.",
"placeholder": "Enter command to execute...",
"output": "Command Output",
"outputPlaceholder": "Command output will appear here...",
"run": "▶️ Run",
"terminate": "⏹️ Stop"
},
"images": {
"title": "🖼️ Image Attachments (Optional)",
"select": "Select Files",
"paste": "Clipboard",
"clear": "Clear",
"status": "{count} images selected",
"statusWithSize": "{count} images selected (Total {size})",
"dragHint": "🎯 Drag images here or press Ctrl+V/Cmd+V to paste from clipboard (PNG, JPG, JPEG, GIF, BMP, WebP)",
"deleteConfirm": "Are you sure you want to remove image \"{filename}\"?",
"deleteTitle": "Confirm Delete",
"sizeWarning": "Image file size cannot exceed 1MB",
"formatError": "Unsupported image format",
"paste_images": "📋 Paste from Clipboard",
"paste_failed": "Paste failed, no image in clipboard",
"paste_no_image": "No image in clipboard to paste",
"paste_image_from_textarea": "Image intelligently pasted from text area to image area",
"images_clear": "Clear all images",
"settings": {
"title": "Image Settings",
"sizeLimit": "Image Size Limit",
"sizeLimitOptions": {
"unlimited": "Unlimited",
"1mb": "1MB",
"3mb": "3MB",
"5mb": "5MB"
},
"base64Detail": "Base64 Compatibility Mode",
"base64DetailHelp": "When enabled, includes complete Base64 image data in text to improve compatibility with AI models ",
"base64Warning": "⚠️ Increases transmission size",
"compatibilityHint": "💡 Images not recognized correctly?",
"enableBase64Hint": "Try enabling Base64 compatibility mode"
},
"sizeLimitExceeded": "Image {filename} size is {size}, exceeds {limit} limit!",
"sizeLimitExceededAdvice": "Please compress the image using image editing software, or adjust the image size limit setting."
},
"language": {
"settings": "Language Settings",
"selector": "🌐 Language Selection",
"description": "Choose your preferred interface language. Language changes take effect immediately."
},
"settings": {
"title": "Application Settings",
"language": {
"title": "Language Settings",
"selector": "🌐 Language Selection"
},
"layout": {
"title": "Interface Layout",
"separateMode": "Separate Mode",
"separateModeDescription": "AI summary and feedback are in separate tabs",
"combinedVertical": "Combined Mode (Vertical Layout)",
"combinedVerticalDescription": "AI summary on top, feedback input below, both on the same page",
"combinedHorizontal": "Combined Mode (Horizontal Layout)",
"combinedHorizontalDescription": "AI summary on left, feedback input on right, expanding summary viewing area"
},
"window": {
"title": "Window Positioning",
"alwaysCenter": "Always show window at primary screen center"
},
"reset": {
"title": "Reset Settings",
"button": "Reset Settings",
"confirmTitle": "Confirm Reset Settings",
"confirmMessage": "Are you sure you want to reset all settings? This will clear all saved preferences and restore to default state.",
"successTitle": "Reset Successful",
"successMessage": "All settings have been successfully reset to default values.",
"errorTitle": "Reset Failed",
"errorMessage": "Error occurred while resetting settings: {error}"
}
},
"timeout": {
"enable": "Auto Close",
"enableTooltip": "When enabled, the interface will automatically close after the specified time",
"duration": {
"label": "Timeout Duration",
"description": "Set the auto-close time (30 seconds - 2 hours)"
},
"seconds": "seconds",
"remaining": "Time Remaining",
"expired": "Time Expired",
"autoCloseMessage": "Interface will automatically close in {seconds} seconds",
"settings": {
"title": "Timeout Settings",
"description": "When enabled, the interface will automatically close after the specified time. The countdown timer will be displayed in the header area."
}
},
"buttons": {
"submit": "Submit Feedback",
"cancel": "Cancel",
"close": "Close",
"clear": "Clear",
"submitFeedback": "✅ Submit Feedback",
"selectFiles": "📁 Select Files",
"pasteClipboard": "📋 Clipboard",
"clearAll": "✕ Clear",
"runCommand": "▶️ Run"
},
"status": {
"feedbackSubmitted": "Feedback submitted successfully!",
"feedbackCancelled": "Feedback cancelled.",
"timeoutMessage": "Feedback timeout",
"errorOccurred": "Error occurred",
"loading": "Loading...",
"connecting": "Connecting...",
"connected": "Connected",
"disconnected": "Disconnected",
"uploading": "Uploading...",
"uploadSuccess": "Upload successful",
"uploadFailed": "Upload failed",
"commandRunning": "Command running...",
"commandFinished": "Command finished",
"pasteSuccess": "Image pasted from clipboard",
"pasteFailed": "Failed to get image from clipboard",
"invalidFileType": "Unsupported file type",
"fileTooLarge": "File too large (max 1MB)"
},
"errors": {
"title": "Error",
"warning": "Warning",
"info": "Information",
"interfaceReloadError": "Error occurred while reloading interface: {error}",
"imageSaveEmpty": "Saved image file is empty! Location: {path}",
"imageSaveFailed": "Image save failed!",
"clipboardSaveFailed": "Failed to save clipboard image!",
"noValidImage": "No valid image in clipboard!",
"noImageContent": "No image content in clipboard!",
"emptyFile": "Image {filename} is an empty file!",
"loadImageFailed": "Failed to load image {filename}:\n{error}",
"dragInvalidFiles": "Please drag valid image files!",
"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!"
},
"languageSelector": "🌐 Language",
"languageNames": {
"zhTw": "繁體中文",
"en": "English",
"zhCn": "简体中文"
},
"test": {
"qtGuiSummary": "🎯 Image Preview and Window Adjustment Test\n\nThis is a test session to verify the following features:\n\n✅ Test Items:\n1. Image upload and preview functionality\n2. Image X delete button in top-right corner\n3. Free window resizing\n4. Flexible splitter adjustment\n5. Dynamic layout of all areas\n6. Smart Ctrl+V image paste functionality\n\n📋 Test Steps:\n1. Try uploading some images (drag & drop, file selection, clipboard)\n2. Check if image preview displays correctly\n3. Click the X button in the top-right corner of images to delete them\n4. Try resizing the window, check if it can be freely adjusted\n5. Drag the splitter to adjust area sizes\n6. Press Ctrl+V in the text box to test smart paste functionality\n7. Provide any feedback or issues found\n\nPlease test these features and provide feedback!",
"webUiSummary": "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 paste\n- Multi-language interface switching\n\n📋 **Test Steps:**\n1. Test image upload (drag & drop, file selection, clipboard)\n2. Press Ctrl+V in text box to test smart paste\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

@@ -0,0 +1,202 @@
{
"meta": {
"language": "zh-CN",
"displayName": "简体中文",
"author": "Minidoracat",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
},
"app": {
"title": "交互式反馈收集",
"projectDirectory": "项目目录",
"language": "语言",
"settings": "设置",
"confirmCancel": "确认取消",
"confirmCancelMessage": "确定要取消反馈吗?所有输入的内容将会丢失。",
"layoutChangeTitle": "界面布局变更",
"layoutChangeMessage": "布局模式已变更,需要重新加载界面才能生效。\n是否现在重新加载"
},
"tabs": {
"summary": "📋 AI 摘要",
"feedback": "💬 反馈",
"command": "⚡ 命令",
"language": "⚙️ 设置",
"images": "🖼️ 图片",
"about": " 关于"
},
"feedback": {
"title": "您的反馈",
"description": "请描述您对 AI 工作结果的想法、建议或需要修改的地方。",
"placeholder": "请在这里输入您的反馈、建议或问题...\n\n💡 小提示:\n• 按 Ctrl+Enter支持数字键盘可快速提交反馈\n• 按 Ctrl+V 可直接粘贴剪贴板图片",
"emptyTitle": "反馈内容为空",
"emptyMessage": "请先输入反馈内容再提交。您可以描述想法、建议或需要修改的地方。"
},
"summary": {
"title": "AI 工作摘要",
"description": "以下是 AI 刚才为您完成的工作内容,请检视并提供反馈。",
"testDescription": "以下是 AI 回复的消息内容,请检视并提供反馈。"
},
"command": {
"title": "命令执行",
"description": "您可以执行命令来验证结果或收集更多信息。",
"placeholder": "输入要执行的命令...",
"output": "命令输出",
"outputPlaceholder": "命令输出将在这里显示...",
"run": "▶️ 执行",
"terminate": "⏹️ 停止"
},
"images": {
"title": "🖼️ 图片附件(可选)",
"select": "选择文件",
"paste": "剪贴板",
"clear": "清除",
"status": "已选择 {count} 张图片",
"statusWithSize": "已选择 {count} 张图片 (总计 {size})",
"dragHint": "🎯 拖拽图片到这里 或 按 Ctrl+V/Cmd+V 粘贴剪贴板图片 (PNG、JPG、JPEG、GIF、BMP、WebP)",
"deleteConfirm": "确定要移除图片 \"{filename}\" 吗?",
"deleteTitle": "确认删除",
"sizeWarning": "图片文件大小不能超过 1MB",
"formatError": "不支持的图片格式",
"paste_images": "📋 从剪贴板粘贴",
"paste_failed": "粘贴失败,剪贴板中没有图片",
"paste_no_image": "剪贴板中没有图片可粘贴",
"paste_image_from_textarea": "已将图片从文本框智能贴到图片区域",
"images_clear": "清除所有图片",
"settings": {
"title": "图片设置",
"sizeLimit": "图片大小限制",
"sizeLimitOptions": {
"unlimited": "无限制",
"1mb": "1MB",
"3mb": "3MB",
"5mb": "5MB"
},
"base64Detail": "Base64 兼容模式",
"base64DetailHelp": "启用后会在文本中包含完整的 Base64 图片数据,提升部分 AI 模型的兼容性",
"base64Warning": "⚠️ 会增加传输量",
"compatibilityHint": "💡 图片无法正确识别?",
"enableBase64Hint": "尝试启用 Base64 兼容模式"
},
"sizeLimitExceeded": "图片 {filename} 大小为 {size},超过 {limit} 限制!",
"sizeLimitExceededAdvice": "建议使用图片编辑软件压缩后再上传,或调整图片大小限制设置。"
},
"settings": {
"title": "应用设置",
"language": {
"title": "语言设置",
"selector": "🌐 语言选择"
},
"layout": {
"title": "界面布局",
"separateMode": "分离模式",
"separateModeDescription": "AI 摘要和反馈分别在不同页签",
"combinedVertical": "合并模式(垂直布局)",
"combinedVerticalDescription": "AI 摘要在上,反馈输入在下,摘要和反馈在同一页面",
"combinedHorizontal": "合并模式(水平布局)",
"combinedHorizontalDescription": "AI 摘要在左,反馈输入在右,增大摘要可视区域"
},
"window": {
"title": "窗口定位",
"alwaysCenter": "总是在主屏幕中心显示窗口"
},
"reset": {
"title": "重置设置",
"button": "重置设置",
"confirmTitle": "确认重置设置",
"confirmMessage": "确定要重置所有设置吗?这将清除所有已保存的偏好设置并恢复到默认状态。",
"successTitle": "重置成功",
"successMessage": "所有设置已成功重置为默认值。",
"errorTitle": "重置失败",
"errorMessage": "重置设置时发生错误:{error}"
}
},
"timeout": {
"enable": "自动关闭",
"enableTooltip": "启用后将在指定时间后自动关闭界面",
"duration": {
"label": "超时时间",
"description": "设置自动关闭的时间30秒 - 2小时"
},
"seconds": "秒",
"remaining": "剩余时间",
"expired": "时间已到",
"autoCloseMessage": "界面将在 {seconds} 秒后自动关闭",
"settings": {
"title": "超时设置",
"description": "启用后,界面将在指定时间后自动关闭。倒数计时器会显示在顶部区域。"
}
},
"buttons": {
"submit": "提交反馈",
"cancel": "取消",
"close": "关闭",
"clear": "清除",
"submitFeedback": "✅ 提交反馈",
"selectFiles": "📁 选择文件",
"pasteClipboard": "📋 剪贴板",
"clearAll": "✕ 清除",
"runCommand": "▶️ 执行"
},
"status": {
"feedbackSubmitted": "反馈已成功提交!",
"feedbackCancelled": "已取消反馈。",
"timeoutMessage": "等待反馈超时",
"errorOccurred": "发生错误",
"loading": "加载中...",
"connecting": "连接中...",
"connected": "已连接",
"disconnected": "连接中断",
"uploading": "上传中...",
"uploadSuccess": "上传成功",
"uploadFailed": "上传失败",
"commandRunning": "命令执行中...",
"commandFinished": "命令执行完成",
"pasteSuccess": "已从剪贴板粘贴图片",
"pasteFailed": "无法从剪贴板获取图片",
"invalidFileType": "不支持的文件类型",
"fileTooLarge": "文件过大(最大 1MB"
},
"errors": {
"title": "错误",
"warning": "警告",
"info": "提示",
"interfaceReloadError": "重新加载界面时发生错误: {error}",
"imageSaveEmpty": "保存的图片文件为空!位置: {path}",
"imageSaveFailed": "图片保存失败!",
"clipboardSaveFailed": "无法保存剪贴板图片!",
"noValidImage": "剪贴板中没有有效的图片!",
"noImageContent": "剪贴板中没有图片内容!",
"emptyFile": "图片 {filename} 是空文件!",
"loadImageFailed": "无法加载图片 {filename}:\n{error}",
"dragInvalidFiles": "请拖拽有效的图片文件!",
"confirmClearAll": "确定要清除所有 {count} 张图片吗?",
"confirmClearTitle": "确认清除",
"fileSizeExceeded": "图片 {filename} 大小为 {size}MB超过 1MB 限制!\n建议使用图片编辑软件压缩后再上传。",
"dataSizeExceeded": "图片 {filename} 数据大小超过 1MB 限制!"
},
"aiSummary": "AI 工作摘要",
"languageSelector": "🌐 语言选择",
"languageNames": {
"zhTw": "繁體中文",
"en": "English",
"zhCn": "简体中文"
},
"test": {
"qtGuiSummary": "🎯 图片预览和窗口调整测试\n\n这是一个测试会话用于验证以下功能\n\n✅ 功能测试项目:\n1. 图片上传和预览功能\n2. 图片右上角X删除按钮\n3. 窗口自由调整大小\n4. 分割器的灵活调整\n5. 各区域的动态布局\n6. 智能 Ctrl+V 图片粘贴功能\n\n📋 测试步骤:\n1. 尝试上传一些图片(拖拽、文件选择、剪贴板)\n2. 检查图片预览是否正常显示\n3. 点击图片右上角的X按钮删除图片\n4. 尝试调整窗口大小,检查是否可以自由调整\n5. 拖动分割器调整各区域大小\n6. 在文本框内按 Ctrl+V 测试智能粘贴功能\n7. 提供任何回馈或发现的问题\n\n请测试这些功能并提供回馈",
"webUiSummary": "测试 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请测试这些功能并提供回馈"
},
"about": {
"appInfo": "应用程序信息",
"version": "版本",
"description": "一个强大的 MCP 服务器,为 AI 辅助开发工具提供人在回路的交互反馈功能。支持 Qt GUI 和 Web UI 双界面,并具备图片上传、命令执行、多语言等丰富功能。",
"projectLinks": "项目链接",
"githubProject": "GitHub 项目",
"visitGithub": "访问 GitHub",
"contact": "联系与支持",
"discordSupport": "Discord 支持",
"joinDiscord": "加入 Discord",
"contactDescription": "如需技术支持、问题反馈或功能建议,欢迎通过 Discord 社群或 GitHub Issues 与我们联系。",
"thanks": "致谢与贡献",
"thanksText": "感谢原作者 Fábio Ferreira (@fabiomlferreira) 创建了原始的 interactive-feedback-mcp 项目。\n\n本增强版本由 Minidoracat 开发和维护,大幅扩展了项目功能,新增了 GUI 界面、图片支持、多语言能力以及许多其他改进功能。\n\n同时感谢 sanshao85 的 mcp-feedback-collector 项目提供的 UI 设计灵感。\n\n开源协作让技术变得更美好"
}
}

View File

@@ -0,0 +1,202 @@
{
"meta": {
"language": "zh-TW",
"displayName": "繁體中文",
"author": "Minidoracat",
"version": "1.0.0",
"lastUpdate": "2025-01-31"
},
"app": {
"title": "互動式回饋收集",
"projectDirectory": "專案目錄",
"language": "語言",
"settings": "設定",
"confirmCancel": "確認取消",
"confirmCancelMessage": "確定要取消回饋嗎?所有輸入的內容將會遺失。",
"layoutChangeTitle": "界面佈局變更",
"layoutChangeMessage": "佈局模式已變更,需要重新載入界面才能生效。\n是否現在重新載入"
},
"tabs": {
"summary": "📋 AI 摘要",
"feedback": "💬 回饋",
"command": "⚡ 命令",
"language": "⚙️ 設置",
"images": "🖼️ 圖片",
"about": " 關於"
},
"about": {
"appInfo": "應用程式資訊",
"version": "版本",
"description": "一個強大的 MCP 伺服器,為 AI 輔助開發工具提供人在回路的互動回饋功能。支援 Qt GUI 和 Web UI 雙介面,並具備圖片上傳、命令執行、多語言等豐富功能。",
"projectLinks": "專案連結",
"githubProject": "GitHub 專案",
"visitGithub": "訪問 GitHub",
"contact": "聯繫與支援",
"discordSupport": "Discord 支援",
"joinDiscord": "加入 Discord",
"contactDescription": "如需技術支援、問題回報或功能建議,歡迎透過 Discord 社群或 GitHub Issues 與我們聯繫。",
"thanks": "致謝與貢獻",
"thanksText": "感謝原作者 Fábio Ferreira (@fabiomlferreira) 創建了原始的 interactive-feedback-mcp 專案。\n\n本增強版本由 Minidoracat 開發和維護,大幅擴展了專案功能,新增了 GUI 介面、圖片支援、多語言能力以及許多其他改進功能。\n\n同時感謝 sanshao85 的 mcp-feedback-collector 專案提供的 UI 設計靈感。\n\n開源協作讓技術變得更美好"
},
"feedback": {
"title": "您的回饋",
"description": "請描述您對 AI 工作結果的想法、建議或需要修改的地方。",
"placeholder": "請在這裡輸入您的回饋、建議或問題...\n\n💡 小提示:\n• 按 Ctrl+Enter支援數字鍵盤可快速提交回饋\n• 按 Ctrl+V 可直接貼上剪貼簿圖片",
"emptyTitle": "回饋內容為空",
"emptyMessage": "請先輸入回饋內容再提交。您可以描述想法、建議或需要修改的地方。",
"input": "您的回饋"
},
"summary": {
"title": "AI 工作摘要",
"description": "以下是 AI 剛才為您完成的工作內容,請檢視並提供回饋。",
"testDescription": "以下是 AI 回復的訊息內容,請檢視並提供回饋。"
},
"command": {
"title": "命令執行",
"description": "您可以執行命令來驗證結果或收集更多資訊。",
"input": "命令",
"placeholder": "輸入要執行的命令...",
"output": "命令輸出",
"outputPlaceholder": "命令輸出將顯示在這裡...",
"run": "▶️ 執行",
"terminate": "⏹️ 終止"
},
"images": {
"title": "🖼️ 圖片附件(可選)",
"select": "選擇文件",
"paste": "剪貼板",
"clear": "清除",
"status": "已選擇 {count} 張圖片",
"statusWithSize": "已選擇 {count} 張圖片 (總計 {size})",
"dragHint": "🎯 拖拽圖片到這裡 或 按 Ctrl+V/Cmd+V 貼上剪貼簿圖片 (PNG、JPG、JPEG、GIF、BMP、WebP)",
"deleteConfirm": "確定要移除圖片 \"{filename}\" 嗎?",
"deleteTitle": "確認刪除",
"sizeWarning": "圖片文件大小不能超過 1MB",
"formatError": "不支援的圖片格式",
"paste_images": "📋 從剪貼簿貼上",
"paste_failed": "貼上失敗,剪貼簿中沒有圖片",
"paste_no_image": "剪貼簿中沒有圖片可貼上",
"paste_image_from_textarea": "已將圖片從文字框智能貼到圖片區域",
"images_clear": "清除所有圖片",
"settings": {
"title": "圖片設定",
"sizeLimit": "圖片大小限制",
"sizeLimitOptions": {
"unlimited": "無限制",
"1mb": "1MB",
"3mb": "3MB",
"5mb": "5MB"
},
"base64Detail": "Base64 相容模式",
"base64DetailHelp": "啟用後會在文字中包含完整的 Base64 圖片資料,提升部分 AI 模型的相容性",
"base64Warning": "⚠️ 會增加傳輸量",
"compatibilityHint": "💡 圖片無法正確識別?",
"enableBase64Hint": "嘗試啟用 Base64 相容模式"
},
"sizeLimitExceeded": "圖片 {filename} 大小為 {size},超過 {limit} 限制!",
"sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。"
},
"timeout": {
"enable": "自動關閉",
"enableTooltip": "啟用後將在指定時間後自動關閉介面",
"duration": {
"label": "超時時間",
"description": "設置自動關閉的時間30秒 - 2小時"
},
"seconds": "秒",
"remaining": "剩餘時間",
"expired": "時間已到",
"autoCloseMessage": "介面將在 {seconds} 秒後自動關閉",
"settings": {
"title": "超時設置",
"description": "啟用後,介面將在指定時間後自動關閉。倒數計時器會顯示在頂部區域。"
}
},
"settings": {
"title": "應用設置",
"language": {
"title": "語言設置",
"selector": "🌐 語言選擇"
},
"layout": {
"title": "界面佈局",
"separateMode": "分離模式",
"separateModeDescription": "AI 摘要和回饋分別在不同頁籤",
"combinedVertical": "合併模式(垂直布局)",
"combinedVerticalDescription": "AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面",
"combinedHorizontal": "合併模式(水平布局)",
"combinedHorizontalDescription": "AI 摘要在左,回饋輸入在右,增大摘要可視區域"
},
"window": {
"title": "視窗定位",
"alwaysCenter": "總是在主螢幕中心顯示視窗"
},
"reset": {
"title": "重置設定",
"button": "重置設定",
"confirmTitle": "確認重置設定",
"confirmMessage": "確定要重置所有設定嗎?這將清除所有已保存的偏好設定並恢復到預設狀態。",
"successTitle": "重置成功",
"successMessage": "所有設定已成功重置為預設值。",
"errorTitle": "重置失敗",
"errorMessage": "重置設定時發生錯誤:{error}"
}
},
"buttons": {
"submit": "提交回饋",
"cancel": "取消",
"close": "關閉",
"clear": "清除",
"submitFeedback": "✅ 提交回饋",
"selectFiles": "📁 選擇文件",
"pasteClipboard": "📋 剪貼板",
"clearAll": "✕ 清除",
"runCommand": "▶️ 執行"
},
"status": {
"feedbackSubmitted": "回饋已成功提交!",
"feedbackCancelled": "已取消回饋。",
"timeoutMessage": "等待回饋超時",
"errorOccurred": "發生錯誤",
"loading": "載入中...",
"connecting": "連接中...",
"connected": "已連接",
"disconnected": "連接中斷",
"uploading": "上傳中...",
"uploadSuccess": "上傳成功",
"uploadFailed": "上傳失敗",
"commandRunning": "命令執行中...",
"commandFinished": "命令執行完成",
"pasteSuccess": "已從剪貼板貼上圖片",
"pasteFailed": "無法從剪貼板獲取圖片",
"invalidFileType": "不支援的文件類型",
"fileTooLarge": "文件過大(最大 1MB"
},
"errors": {
"title": "錯誤",
"warning": "警告",
"info": "提示",
"interfaceReloadError": "重新載入界面時發生錯誤: {error}",
"imageSaveEmpty": "保存的圖片文件為空!位置: {path}",
"imageSaveFailed": "圖片保存失敗!",
"clipboardSaveFailed": "無法保存剪貼板圖片!",
"noValidImage": "剪貼板中沒有有效的圖片!",
"noImageContent": "剪貼板中沒有圖片內容!",
"emptyFile": "圖片 {filename} 是空文件!",
"loadImageFailed": "無法載入圖片 {filename}:\n{error}",
"dragInvalidFiles": "請拖拽有效的圖片文件!",
"confirmClearAll": "確定要清除所有 {count} 張圖片嗎?",
"confirmClearTitle": "確認清除",
"fileSizeExceeded": "圖片 {filename} 大小為 {size}MB超過 1MB 限制!\n建議使用圖片編輯軟體壓縮後再上傳。",
"dataSizeExceeded": "圖片 {filename} 數據大小超過 1MB 限制!"
},
"languageNames": {
"zhTw": "繁體中文",
"en": "English",
"zhCn": "简体中文"
},
"test": {
"qtGuiSummary": "🎯 圖片預覽和視窗調整測試\n\n這是一個測試會話用於驗證以下功能\n\n✅ 功能測試項目:\n1. 圖片上傳和預覽功能\n2. 圖片右上角X刪除按鈕\n3. 視窗自由調整大小\n4. 分割器的靈活調整\n5. 各區域的動態佈局\n6. 智能 Ctrl+V 圖片貼上功能\n\n📋 測試步驟:\n1. 嘗試上傳一些圖片(拖拽、文件選擇、剪貼板)\n2. 檢查圖片預覽是否正常顯示\n3. 點擊圖片右上角的X按鈕刪除圖片\n4. 嘗試調整視窗大小,檢查是否可以自由調整\n5. 拖動分割器調整各區域大小\n6. 在文字框內按 Ctrl+V 測試智能貼上功能\n7. 提供任何回饋或發現的問題\n\n請測試這些功能並提供回饋",
"webUiSummary": "測試 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

@@ -0,0 +1,147 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI 主要入口點
==============
提供 GUI 回饋介面的主要入口點函數。
"""
import threading
import time
from typing import Optional
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtGui import QFont
from PySide6.QtCore import QTimer
import sys
from .models import FeedbackResult
from .window import FeedbackWindow
def feedback_ui(project_directory: str, summary: str) -> Optional[FeedbackResult]:
"""
啟動回饋收集 GUI 介面
Args:
project_directory: 專案目錄路徑
summary: AI 工作摘要
Returns:
Optional[FeedbackResult]: 回饋結果,如果用戶取消則返回 None
"""
# 檢查是否已有 QApplication 實例
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 設定全域微軟正黑體字體
font = QFont("Microsoft JhengHei", 11) # 微軟正黑體11pt
app.setFont(font)
# 設定字體回退順序,確保中文字體正確顯示
app.setStyleSheet("""
* {
font-family: "Microsoft JhengHei", "微軟正黑體", "Microsoft YaHei", "微软雅黑", "SimHei", "黑体", sans-serif;
}
""")
# 創建主窗口
window = FeedbackWindow(project_directory, summary)
window.show()
# 運行事件循環直到窗口關閉
app.exec()
# 返回結果
return window.result
def feedback_ui_with_timeout(project_directory: str, summary: str, timeout: int) -> Optional[FeedbackResult]:
"""
啟動帶超時的回饋收集 GUI 介面
Args:
project_directory: 專案目錄路徑
summary: AI 工作摘要
timeout: 超時時間(秒)- MCP 傳入的超時時間,作為最大限制
Returns:
Optional[FeedbackResult]: 回饋結果,如果用戶取消或超時則返回 None
Raises:
TimeoutError: 當超時時拋出
"""
# 檢查是否已有 QApplication 實例
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 設定全域微軟正黑體字體
font = QFont("Microsoft JhengHei", 11) # 微軟正黑體11pt
app.setFont(font)
# 設定字體回退順序,確保中文字體正確顯示
app.setStyleSheet("""
* {
font-family: "Microsoft JhengHei", "微軟正黑體", "Microsoft YaHei", "微软雅黑", "SimHei", "黑体", sans-serif;
}
""")
# 創建主窗口,傳入 MCP 超時時間
window = FeedbackWindow(project_directory, summary, timeout)
# 連接超時信號
timeout_occurred = False
def on_timeout():
nonlocal timeout_occurred
timeout_occurred = True
window.timeout_occurred.connect(on_timeout)
window.show()
# 開始用戶設置的超時倒數(如果啟用)
window.start_timeout_if_enabled()
# 創建 MCP 超時計時器作為後備
mcp_timeout_timer = QTimer()
mcp_timeout_timer.setSingleShot(True)
mcp_timeout_timer.timeout.connect(lambda: _handle_mcp_timeout(window, app))
mcp_timeout_timer.start(timeout * 1000) # 轉換為毫秒
# 運行事件循環直到窗口關閉
app.exec()
# 停止計時器(如果還在運行)
mcp_timeout_timer.stop()
window.stop_timeout()
# 檢查是否超時
if timeout_occurred:
raise TimeoutError(f"回饋收集超時GUI 介面已自動關閉")
elif hasattr(window, '_timeout_occurred'):
raise TimeoutError(f"回饋收集超時({timeout}GUI 介面已自動關閉")
# 返回結果
return window.result
def _handle_timeout(window: FeedbackWindow, app: QApplication) -> None:
"""處理超時事件(舊版本,保留向後兼容)"""
# 標記超時發生
window._timeout_occurred = True
# 強制關閉視窗
window.force_close()
# 退出應用程式
app.quit()
def _handle_mcp_timeout(window: FeedbackWindow, app: QApplication) -> None:
"""處理 MCP 超時事件(後備機制)"""
# 標記超時發生
window._timeout_occurred = True
# 強制關閉視窗
window.force_close()
# 退出應用程式
app.quit()

View File

@@ -0,0 +1,10 @@
"""
GUI 資料模型模組
===============
定義 GUI 相關的資料結構和型別。
"""
from .feedback import FeedbackResult
__all__ = ['FeedbackResult']

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回饋結果資料模型
===============
定義回饋收集的資料結構。
"""
from typing import TypedDict, List
class FeedbackResult(TypedDict):
"""回饋結果的型別定義"""
command_logs: str
interactive_feedback: str
images: List[dict]

View File

@@ -0,0 +1,17 @@
"""
GUI 樣式模組
============
集中管理 GUI 的樣式定義。
"""
from .themes import *
__all__ = [
'BUTTON_BASE_STYLE',
'PRIMARY_BUTTON_STYLE',
'SUCCESS_BUTTON_STYLE',
'DANGER_BUTTON_STYLE',
'SECONDARY_BUTTON_STYLE',
'DARK_STYLE'
]

View File

@@ -0,0 +1,277 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI 主題樣式定義
===============
集中定義所有 GUI 元件的樣式。
"""
# 統一按鈕樣式常量
BUTTON_BASE_STYLE = """
QPushButton {
color: white;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 12px;
}
QPushButton:hover {
opacity: 0.8;
}
"""
PRIMARY_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #0e639c;
}
QPushButton:hover {
background-color: #005a9e;
}
"""
SUCCESS_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #4caf50;
}
QPushButton:hover {
background-color: #45a049;
}
"""
DANGER_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #f44336;
color: #ffffff;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
"""
SECONDARY_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #666666;
}
QPushButton:hover {
background-color: #555555;
}
"""
# Dark 主題樣式
DARK_STYLE = """
QMainWindow {
background-color: #1e1e1e;
color: #d4d4d4;
}
QWidget {
background-color: #1e1e1e;
color: #d4d4d4;
}
QLabel {
color: #d4d4d4;
}
QLineEdit {
background-color: #333333;
border: 1px solid #464647;
padding: 8px;
border-radius: 6px;
color: #d4d4d4;
font-size: 14px;
}
QLineEdit:focus {
border-color: #007acc;
background-color: #383838;
}
QTextEdit {
background-color: #333333;
border: 1px solid #464647;
padding: 8px;
border-radius: 6px;
color: #d4d4d4;
font-size: 14px;
line-height: 1.5;
}
QTextEdit:focus {
border-color: #007acc;
background-color: #383838;
}
QGroupBox {
font-weight: bold;
border: 2px solid #464647;
border-radius: 6px;
margin-top: 6px;
padding-top: 10px;
background-color: #2d2d30;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 8px;
background-color: #2d2d30;
color: #007acc;
}
QTabWidget::pane {
border: 1px solid #464647;
background-color: #2d2d30;
border-radius: 6px;
}
QTabBar::tab {
background-color: #3c3c3c;
color: #d4d4d4;
padding: 8px 12px;
margin-right: 2px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
QTabBar::tab:selected {
background-color: #007acc;
color: white;
}
QTabBar::tab:hover {
background-color: #4a4a4a;
}
QComboBox {
background-color: #333333;
border: 1px solid #464647;
padding: 6px 8px;
border-radius: 6px;
color: #d4d4d4;
font-size: 14px;
min-width: 120px;
}
QComboBox:focus {
border-color: #007acc;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #d4d4d4;
margin-top: 2px;
}
QComboBox QAbstractItemView {
background-color: #333333;
border: 1px solid #464647;
color: #d4d4d4;
selection-background-color: #007acc;
}
QScrollBar:vertical {
background-color: #333333;
width: 12px;
border: none;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #555555;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #777777;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
background: none;
}
QScrollBar:horizontal {
background-color: #333333;
height: 12px;
border: none;
border-radius: 6px;
}
QScrollBar::handle:horizontal {
background-color: #555555;
border-radius: 6px;
min-width: 20px;
}
QScrollBar::handle:horizontal:hover {
background-color: #777777;
}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
border: none;
background: none;
}
QMenuBar {
background-color: #2d2d30;
color: #d4d4d4;
border-bottom: 1px solid #464647;
}
QMenuBar::item {
background-color: transparent;
padding: 4px 8px;
}
QMenuBar::item:selected {
background-color: #007acc;
}
QMenu {
background-color: #2d2d30;
color: #d4d4d4;
border: 1px solid #464647;
}
QMenu::item {
padding: 6px 12px;
}
QMenu::item:selected {
background-color: #007acc;
}
QSplitter::handle {
background-color: #464647;
}
QSplitter::handle:horizontal {
width: 3px;
}
QSplitter::handle:vertical {
height: 3px;
}
/* 訊息框樣式 */
QMessageBox {
background-color: #2d2d30;
color: #d4d4d4;
}
QMessageBox QPushButton {
min-width: 60px;
padding: 6px 12px;
}
"""

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
分頁組件
========
包含各種專用分頁組件的實現。
"""
from .feedback_tab import FeedbackTab
from .summary_tab import SummaryTab
from .command_tab import CommandTab
from .settings_tab import SettingsTab
from .about_tab import AboutTab
__all__ = [
'FeedbackTab',
'SummaryTab',
'CommandTab',
'SettingsTab',
'AboutTab'
]

View File

@@ -0,0 +1,261 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
關於分頁組件
============
顯示應用程式資訊和聯繫方式的分頁組件。
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QPushButton, QTextEdit, QScrollArea
)
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QFont, QDesktopServices
from ...i18n import t
from ... import __version__
class AboutTab(QWidget):
"""關於分頁組件"""
def __init__(self, parent=None):
super().__init__(parent)
self._setup_ui()
def _setup_ui(self) -> None:
"""設置用戶介面"""
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
# 創建滾動區域
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll_area.setStyleSheet("""
QScrollArea {
border: none;
background: transparent;
}
QScrollBar:vertical {
background-color: #2d2d30;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #464647;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #555555;
}
""")
# 創建內容容器
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
content_layout.setSpacing(16)
content_layout.setContentsMargins(16, 16, 16, 16)
# === 主要資訊區域(合併應用程式資訊、專案連結、聯繫與支援) ===
self.main_info_group = QGroupBox(t('about.appInfo'))
self.main_info_group.setObjectName('main_info_group')
main_info_layout = QVBoxLayout(self.main_info_group)
main_info_layout.setSpacing(16)
main_info_layout.setContentsMargins(20, 20, 20, 20)
# 應用程式標題和版本
title_layout = QHBoxLayout()
self.app_title_label = QLabel("MCP Feedback Enhanced")
self.app_title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #e0e0e0;")
title_layout.addWidget(self.app_title_label)
title_layout.addStretch()
self.version_label = QLabel(f"v{__version__}")
self.version_label.setStyleSheet("font-size: 16px; color: #007acc; font-weight: bold;")
title_layout.addWidget(self.version_label)
main_info_layout.addLayout(title_layout)
# 應用程式描述
self.app_description = QLabel(t('about.description'))
self.app_description.setStyleSheet("color: #9e9e9e; font-size: 13px; line-height: 1.4; margin-bottom: 16px;")
self.app_description.setWordWrap(True)
main_info_layout.addWidget(self.app_description)
# 分隔線
separator1 = QLabel()
separator1.setFixedHeight(1)
separator1.setStyleSheet("background-color: #464647; margin: 8px 0;")
main_info_layout.addWidget(separator1)
# GitHub 專案區域
github_layout = QHBoxLayout()
self.github_label = QLabel("📂 " + t('about.githubProject'))
self.github_label.setStyleSheet("font-weight: bold; color: #e0e0e0; font-size: 14px;")
github_layout.addWidget(self.github_label)
github_layout.addStretch()
self.github_button = QPushButton(t('about.visitGithub'))
self.github_button.setFixedSize(120, 32)
self.github_button.setStyleSheet("""
QPushButton {
background-color: #0078d4;
border: none;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: bold;
}
QPushButton:hover {
background-color: #106ebe;
}
QPushButton:pressed {
background-color: #005a9e;
}
""")
self.github_button.clicked.connect(self._open_github)
github_layout.addWidget(self.github_button)
main_info_layout.addLayout(github_layout)
# GitHub URL
self.github_url_label = QLabel("https://github.com/Minidoracat/mcp-feedback-enhanced")
self.github_url_label.setStyleSheet("color: #9e9e9e; font-size: 11px; margin-left: 24px; margin-bottom: 12px;")
main_info_layout.addWidget(self.github_url_label)
# 分隔線
separator2 = QLabel()
separator2.setFixedHeight(1)
separator2.setStyleSheet("background-color: #464647; margin: 8px 0;")
main_info_layout.addWidget(separator2)
# Discord 支援區域
discord_layout = QHBoxLayout()
self.discord_label = QLabel("💬 " + t('about.discordSupport'))
self.discord_label.setStyleSheet("font-weight: bold; color: #e0e0e0; font-size: 14px;")
discord_layout.addWidget(self.discord_label)
discord_layout.addStretch()
self.discord_button = QPushButton(t('about.joinDiscord'))
self.discord_button.setFixedSize(120, 32)
self.discord_button.setStyleSheet("""
QPushButton {
background-color: #5865F2;
border: none;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: bold;
}
QPushButton:hover {
background-color: #4752C4;
}
QPushButton:pressed {
background-color: #3C45A5;
}
""")
self.discord_button.clicked.connect(self._open_discord)
discord_layout.addWidget(self.discord_button)
main_info_layout.addLayout(discord_layout)
# Discord URL 和說明
self.discord_url_label = QLabel("https://discord.gg/ACjf9Q58")
self.discord_url_label.setStyleSheet("color: #9e9e9e; font-size: 11px; margin-left: 24px;")
main_info_layout.addWidget(self.discord_url_label)
self.contact_description = QLabel(t('about.contactDescription'))
self.contact_description.setStyleSheet("color: #9e9e9e; font-size: 12px; margin-left: 24px; margin-top: 8px;")
self.contact_description.setWordWrap(True)
main_info_layout.addWidget(self.contact_description)
content_layout.addWidget(self.main_info_group)
# === 致謝區域 ===
self.thanks_group = QGroupBox(t('about.thanks'))
self.thanks_group.setObjectName('thanks_group')
thanks_layout = QVBoxLayout(self.thanks_group)
thanks_layout.setSpacing(12)
thanks_layout.setContentsMargins(20, 20, 20, 20)
# 致謝文字
self.thanks_text = QTextEdit()
self.thanks_text.setReadOnly(True)
self.thanks_text.setMinimumHeight(160)
self.thanks_text.setMaximumHeight(220)
self.thanks_text.setStyleSheet("""
QTextEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 12px;
color: #e0e0e0;
font-size: 12px;
line-height: 1.4;
}
QScrollBar:vertical {
background-color: #2d2d30;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #464647;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #555555;
}
""")
self.thanks_text.setPlainText(t('about.thanksText'))
thanks_layout.addWidget(self.thanks_text)
content_layout.addWidget(self.thanks_group)
# 添加彈性空間
content_layout.addStretch()
# 設置滾動區域的內容
scroll_area.setWidget(content_widget)
main_layout.addWidget(scroll_area)
def _open_github(self) -> None:
"""開啟 GitHub 專案連結"""
QDesktopServices.openUrl(QUrl("https://github.com/Minidoracat/mcp-feedback-enhanced"))
def _open_discord(self) -> None:
"""開啟 Discord 邀請連結"""
QDesktopServices.openUrl(QUrl("https://discord.gg/ACjf9Q58"))
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
# 更新GroupBox標題
self.main_info_group.setTitle(t('about.appInfo'))
self.thanks_group.setTitle(t('about.thanks'))
# 更新版本資訊
self.version_label.setText(f"v{__version__}")
# 更新描述文字
self.app_description.setText(t('about.description'))
self.contact_description.setText(t('about.contactDescription'))
# 更新標籤文字
self.github_label.setText("📂 " + t('about.githubProject'))
self.discord_label.setText("💬 " + t('about.discordSupport'))
# 更新按鈕文字
self.github_button.setText(t('about.visitGithub'))
self.discord_button.setText(t('about.joinDiscord'))
# 更新致謝文字
self.thanks_text.setPlainText(t('about.thanksText'))

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
命令分頁組件
============
專門處理命令執行的分頁組件。
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QTextEdit, QLineEdit, QPushButton
)
from PySide6.QtCore import Signal
from PySide6.QtGui import QFont
from ..utils import apply_widget_styles
from ..window.command_executor import CommandExecutor
from ...i18n import t
class CommandTab(QWidget):
"""命令分頁組件"""
def __init__(self, project_dir: str, parent=None):
super().__init__(parent)
self.project_dir = project_dir
self.command_executor = CommandExecutor(project_dir, self)
self._setup_ui()
self._connect_signals()
def _setup_ui(self) -> None:
"""設置用戶介面"""
command_layout = QVBoxLayout(self)
command_layout.setSpacing(0) # 緊湊佈局
command_layout.setContentsMargins(0, 0, 0, 0)
# 命令說明區域(頂部,只保留說明文字)
header_widget = QWidget()
header_layout = QVBoxLayout(header_widget)
header_layout.setSpacing(6)
header_layout.setContentsMargins(12, 8, 12, 8)
self.command_description_label = QLabel(t('command.description'))
self.command_description_label.setStyleSheet("color: #9e9e9e; font-size: 11px; margin-bottom: 6px;")
self.command_description_label.setWordWrap(True)
header_layout.addWidget(self.command_description_label)
command_layout.addWidget(header_widget)
# 命令輸出區域(中間,佔大部分空間)
output_widget = QWidget()
output_layout = QVBoxLayout(output_widget)
output_layout.setSpacing(6)
output_layout.setContentsMargins(12, 4, 12, 8)
self.command_output = QTextEdit()
self.command_output.setReadOnly(True)
self.command_output.setFont(QFont("Consolas", 11))
self.command_output.setPlaceholderText(t('command.outputPlaceholder'))
# 終端機風格樣式
self.command_output.setStyleSheet("""
QTextEdit {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
padding: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 11px;
color: #00ff00;
line-height: 1.4;
}
QScrollBar:vertical {
background-color: #2a2a2a;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #555;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #666;
}
""")
output_layout.addWidget(self.command_output, 1) # 佔據剩餘空間
command_layout.addWidget(output_widget, 1) # 輸出區域佔大部分空間
# 命令輸入區域(底部,固定高度)
input_widget = QWidget()
input_widget.setFixedHeight(70) # 固定高度
input_layout = QVBoxLayout(input_widget)
input_layout.setSpacing(6)
input_layout.setContentsMargins(12, 8, 12, 12)
# 命令輸入和執行按鈕(水平布局)
input_row_layout = QHBoxLayout()
input_row_layout.setSpacing(8)
# 提示符號標籤
prompt_label = QLabel("$")
prompt_label.setStyleSheet("color: #00ff00; font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; font-weight: bold;")
prompt_label.setFixedWidth(20)
input_row_layout.addWidget(prompt_label)
self.command_input = QLineEdit()
self.command_input.setPlaceholderText(t('command.placeholder'))
self.command_input.setMinimumHeight(36)
# 終端機風格輸入框
self.command_input.setStyleSheet("""
QLineEdit {
background-color: #1a1a1a;
border: 2px solid #333;
border-radius: 4px;
padding: 8px 12px;
color: #00ff00;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
}
QLineEdit:focus {
border-color: #007acc;
background-color: #1e1e1e;
}
""")
self.command_input.returnPressed.connect(self._run_command)
input_row_layout.addWidget(self.command_input, 1) # 佔據大部分空間
self.command_run_button = QPushButton(t('command.run'))
self.command_run_button.clicked.connect(self._run_command)
self.command_run_button.setFixedSize(80, 36)
apply_widget_styles(self.command_run_button, "primary_button")
input_row_layout.addWidget(self.command_run_button)
self.command_terminate_button = QPushButton(t('command.terminate'))
self.command_terminate_button.clicked.connect(self._terminate_command)
self.command_terminate_button.setFixedSize(80, 36)
apply_widget_styles(self.command_terminate_button, "danger_button")
input_row_layout.addWidget(self.command_terminate_button)
input_layout.addLayout(input_row_layout)
command_layout.addWidget(input_widget) # 輸入區域在底部
def _connect_signals(self) -> None:
"""連接信號"""
self.command_executor.output_received.connect(self._append_command_output)
def _run_command(self) -> None:
"""執行命令"""
command = self.command_input.text().strip()
if command:
self.command_executor.run_command(command)
self.command_input.clear()
def _terminate_command(self) -> None:
"""終止命令"""
self.command_executor.terminate_command()
def _append_command_output(self, text: str) -> None:
"""添加命令輸出並自動滾動到底部"""
# 移動光標到最後
cursor = self.command_output.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
self.command_output.setTextCursor(cursor)
# 插入文本
self.command_output.insertPlainText(text)
# 確保滾動到最底部
scrollbar = self.command_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
# 刷新界面
from PySide6.QtWidgets import QApplication
QApplication.processEvents()
def get_command_logs(self) -> str:
"""獲取命令日誌"""
return self.command_output.toPlainText().strip()
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
self.command_description_label.setText(t('command.description'))
self.command_input.setPlaceholderText(t('command.placeholder'))
self.command_output.setPlaceholderText(t('command.outputPlaceholder'))
self.command_run_button.setText(t('command.run'))
self.command_terminate_button.setText(t('command.terminate'))
def cleanup(self) -> None:
"""清理資源"""
if self.command_executor:
self.command_executor.cleanup()

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回饋分頁組件
============
專門處理用戶回饋輸入的分頁組件。
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QSplitter, QSizePolicy
from PySide6.QtCore import Qt, Signal
from ..widgets import SmartTextEdit, ImageUploadWidget
from ...i18n import t
from ..window.config_manager import ConfigManager
class FeedbackTab(QWidget):
"""回饋分頁組件"""
def __init__(self, parent=None):
super().__init__(parent)
self.config_manager = ConfigManager()
self._setup_ui()
def _setup_ui(self) -> None:
"""設置用戶介面"""
# 主布局
tab_layout = QVBoxLayout(self)
tab_layout.setSpacing(12)
tab_layout.setContentsMargins(0, 0, 0, 0) # 設置邊距為0與合併分頁保持一致
# 說明文字容器
description_wrapper = QWidget()
description_layout = QVBoxLayout(description_wrapper)
description_layout.setContentsMargins(16, 16, 16, 10) # 只對說明文字設置邊距
description_layout.setSpacing(0)
# 說明文字
self.feedback_description = QLabel(t('feedback.description'))
self.feedback_description.setStyleSheet("color: #9e9e9e; font-size: 12px;")
self.feedback_description.setWordWrap(True)
description_layout.addWidget(self.feedback_description)
tab_layout.addWidget(description_wrapper)
# 使用分割器來管理回饋輸入和圖片區域
splitter_wrapper = QWidget() # 創建包裝容器
splitter_wrapper_layout = QVBoxLayout(splitter_wrapper)
splitter_wrapper_layout.setContentsMargins(16, 0, 16, 16) # 設置左右邊距
splitter_wrapper_layout.setSpacing(0)
feedback_splitter = QSplitter(Qt.Vertical)
feedback_splitter.setChildrenCollapsible(False)
feedback_splitter.setHandleWidth(6)
feedback_splitter.setContentsMargins(0, 0, 0, 0) # 設置分割器邊距為0
feedback_splitter.setStyleSheet("""
QSplitter {
border: none;
background: transparent;
}
QSplitter::handle:vertical {
height: 8px;
background-color: #3c3c3c;
border: 1px solid #555555;
border-radius: 4px;
margin-left: 0px;
margin-right: 0px;
margin-top: 2px;
margin-bottom: 2px;
}
QSplitter::handle:vertical:hover {
background-color: #606060;
border-color: #808080;
}
QSplitter::handle:vertical:pressed {
background-color: #007acc;
border-color: #005a9e;
}
""")
# 創建圖片上傳區域(需要先創建以便連接信號)
image_upload_widget = QWidget()
image_upload_widget.setMinimumHeight(200) # 進一步增加最小高度
image_upload_widget.setMaximumHeight(320) # 增加最大高度
image_upload_layout = QVBoxLayout(image_upload_widget)
image_upload_layout.setSpacing(8)
image_upload_layout.setContentsMargins(0, 8, 0, 0) # 與回饋輸入區域保持一致的邊距
self.image_upload = ImageUploadWidget(config_manager=self.config_manager)
image_upload_layout.addWidget(self.image_upload, 1)
# 回饋輸入區域
self.feedback_input = SmartTextEdit()
placeholder_text = t('feedback.placeholder').replace("Ctrl+Enter", "Ctrl+Enter/Cmd+Enter").replace("Ctrl+V", "Ctrl+V/Cmd+V")
self.feedback_input.setPlaceholderText(placeholder_text)
self.feedback_input.setMinimumHeight(120)
self.feedback_input.setStyleSheet("""
QTextEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 10px;
color: #ffffff;
font-size: 12px;
line-height: 1.4;
}
""")
# 直接連接文字輸入框的圖片貼上信號到圖片上傳組件
self.feedback_input.image_paste_requested.connect(self.image_upload.paste_from_clipboard)
# 添加到分割器
feedback_splitter.addWidget(self.feedback_input)
feedback_splitter.addWidget(image_upload_widget)
# 調整分割器比例和設置(確保圖片區域始終可見)
feedback_splitter.setStretchFactor(0, 2) # 回饋輸入區域
feedback_splitter.setStretchFactor(1, 1) # 圖片上傳區域
# 從配置載入分割器位置,如果沒有則使用預設值
saved_sizes = self.config_manager.get_splitter_sizes('feedback_splitter')
if saved_sizes and len(saved_sizes) == 2:
feedback_splitter.setSizes(saved_sizes)
else:
feedback_splitter.setSizes([220, 200]) # 預設大小
# 連接分割器位置變化信號,自動保存位置
feedback_splitter.splitterMoved.connect(
lambda pos, index: self._save_feedback_splitter_position(feedback_splitter)
)
# 設置分割器的最小尺寸和處理策略
feedback_splitter.setMinimumHeight(200) # 降低分割器最小高度,支持小窗口
feedback_splitter.setMaximumHeight(2000) # 允許更大的高度以觸發滾動
# 確保子控件的最小尺寸(防止過度壓縮)
self.feedback_input.setMinimumHeight(80) # 降低文字輸入最小高度
image_upload_widget.setMinimumHeight(100) # 降低圖片區域最小高度
splitter_wrapper_layout.addWidget(feedback_splitter)
tab_layout.addWidget(splitter_wrapper, 1)
# 設置分頁的大小策略,確保能夠觸發父容器的滾動條
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
self.setMinimumHeight(200) # 降低回饋分頁最小高度 # 設置最小高度
def get_feedback_text(self) -> str:
"""獲取回饋文字"""
return self.feedback_input.toPlainText().strip()
def get_images_data(self) -> list:
"""獲取圖片數據"""
return self.image_upload.get_images_data()
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
self.feedback_description.setText(t('feedback.description'))
placeholder_text = t('feedback.placeholder').replace("Ctrl+Enter", "Ctrl+Enter/Cmd+Enter").replace("Ctrl+V", "Ctrl+V/Cmd+V")
self.feedback_input.setPlaceholderText(placeholder_text)
if hasattr(self, 'image_upload'):
self.image_upload.update_texts()
def _save_feedback_splitter_position(self, splitter: QSplitter) -> None:
"""保存分割器的位置"""
sizes = splitter.sizes()
self.config_manager.set_splitter_sizes('feedback_splitter', sizes)

View File

@@ -0,0 +1,623 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
設置分頁組件
============
專門處理應用設置的分頁組件。
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QRadioButton, QButtonGroup, QMessageBox,
QCheckBox, QPushButton, QFrame, QSpinBox
)
from ..widgets import SwitchWithLabel
from ..widgets.styled_spinbox import StyledSpinBox
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QFont
from ...i18n import t, get_i18n_manager
from ...debug import gui_debug_log as debug_log
class SettingsTab(QWidget):
"""設置分頁組件"""
language_changed = Signal()
layout_change_requested = Signal(bool, str) # 佈局變更請求信號 (combined_mode, orientation)
reset_requested = Signal() # 重置設定請求信號
timeout_settings_changed = Signal(bool, int) # 超時設置變更信號 (enabled, duration)
def __init__(self, combined_mode: bool, config_manager, parent=None):
super().__init__(parent)
self.combined_mode = combined_mode
self.config_manager = config_manager
self.layout_orientation = self.config_manager.get_layout_orientation()
self.i18n = get_i18n_manager()
# 保存需要更新的UI元素引用
self.ui_elements = {}
# 設置全域字體為微軟正黑體
self._setup_font()
self._setup_ui()
# 在UI設置完成後確保正確設置初始狀態
self._set_initial_layout_state()
def _setup_font(self) -> None:
"""設置全域字體"""
font = QFont("Microsoft JhengHei", 9) # 微軟正黑體,調整為 9pt
self.setFont(font)
# 設置整個控件的樣式表,確保中文字體正確
self.setStyleSheet("""
QWidget {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
}
""")
def _setup_ui(self) -> None:
"""設置用戶介面"""
# 主容器
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# 左側內容區域
content_widget = QWidget()
content_widget.setMaximumWidth(600)
content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(20, 20, 20, 20)
content_layout.setSpacing(16)
# === 語言設置 ===
self._create_language_section(content_layout)
# 添加分隔線
self._add_separator(content_layout)
# === 界面佈局 ===
self._create_layout_section(content_layout)
# 添加分隔線
self._add_separator(content_layout)
# === 視窗設置 ===
self._create_window_section(content_layout)
# 添加分隔線
self._add_separator(content_layout)
# === 超時設置 ===
self._create_timeout_section(content_layout)
# 添加分隔線
self._add_separator(content_layout)
# === 重置設定 ===
self._create_reset_section(content_layout)
# 添加彈性空間
content_layout.addStretch()
# 添加到主布局
main_layout.addWidget(content_widget)
main_layout.addStretch() # 右側彈性空間
# 設定初始狀態
self._set_initial_layout_state()
def _add_separator(self, layout: QVBoxLayout) -> None:
"""添加分隔線"""
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setStyleSheet("""
QFrame {
color: #444444;
background-color: #444444;
border: none;
height: 1px;
margin: 6px 0px;
}
""")
layout.addWidget(separator)
def _create_section_header(self, title: str, emoji: str = "") -> QLabel:
"""創建區塊標題"""
text = f"{emoji} {title}" if emoji else title
label = QLabel(text)
label.setStyleSheet("""
QLabel {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
font-size: 16px;
font-weight: bold;
color: #ffffff;
margin-bottom: 6px;
margin-top: 2px;
}
""")
return label
def _create_description(self, text: str) -> QLabel:
"""創建說明文字"""
label = QLabel(text)
label.setStyleSheet("""
QLabel {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
color: #aaaaaa;
font-size: 12px;
margin-bottom: 12px;
line-height: 1.3;
}
""")
label.setWordWrap(True)
return label
def _create_language_section(self, layout: QVBoxLayout) -> None:
"""創建語言設置區域"""
header = self._create_section_header(t('settings.language.title'), "🌐")
layout.addWidget(header)
# 保存引用以便更新
self.ui_elements['language_header'] = header
# 語言選擇器容器
lang_container = QHBoxLayout()
lang_container.setContentsMargins(0, 0, 0, 0)
self.language_selector = QComboBox()
self.language_selector.setMinimumHeight(28)
self.language_selector.setMaximumWidth(140)
self.language_selector.setStyleSheet("""
QComboBox {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 4px;
padding: 4px 8px;
color: #ffffff;
font-size: 12px;
}
QComboBox:hover {
border-color: #0078d4;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgNEw2IDdMOSA0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg==);
width: 12px;
height: 12px;
}
QComboBox QAbstractItemView {
selection-background-color: #0078d4;
color: #ffffff;
font-size: 12px;
min-width: 120px;
}
""")
# 填充語言選項
self._populate_language_selector()
self.language_selector.currentIndexChanged.connect(self._on_language_changed)
lang_container.addWidget(self.language_selector)
lang_container.addStretch()
layout.addLayout(lang_container)
def _create_layout_section(self, layout: QVBoxLayout) -> None:
"""創建界面佈局區域"""
header = self._create_section_header(t('settings.layout.title'), "📐")
layout.addWidget(header)
# 保存引用以便更新
self.ui_elements['layout_header'] = header
# 選項容器
options_layout = QVBoxLayout()
options_layout.setSpacing(2)
# 創建按鈕組
self.layout_button_group = QButtonGroup()
# 分離模式
self.separate_mode_radio = QRadioButton(t('settings.layout.separateMode'))
self.separate_mode_radio.setStyleSheet("""
QRadioButton {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
font-size: 13px;
color: #ffffff;
spacing: 8px;
padding: 2px 0px;
}
QRadioButton::indicator {
width: 16px;
height: 16px;
}
QRadioButton::indicator:unchecked {
border: 2px solid #666666;
border-radius: 9px;
background-color: transparent;
}
QRadioButton::indicator:checked {
border: 2px solid #0078d4;
border-radius: 9px;
background-color: #0078d4;
image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSI4IiB2aWV3Qm94PSIwIDAgOCA4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8Y2lyY2xlIGN4PSI0IiBjeT0iNCIgcj0iMiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+);
}
QRadioButton::indicator:hover {
border-color: #0078d4;
}
""")
self.layout_button_group.addButton(self.separate_mode_radio, 0)
options_layout.addWidget(self.separate_mode_radio)
separate_hint = QLabel(f" {t('settings.layout.separateModeDescription')}")
separate_hint.setStyleSheet("""
QLabel {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
color: #888888;
font-size: 11px;
margin-left: 20px;
margin-bottom: 4px;
}
""")
options_layout.addWidget(separate_hint)
# 保存引用以便更新
self.ui_elements['separate_hint'] = separate_hint
# 合併模式(垂直)
self.combined_vertical_radio = QRadioButton(t('settings.layout.combinedVertical'))
self.combined_vertical_radio.setStyleSheet(self.separate_mode_radio.styleSheet())
self.layout_button_group.addButton(self.combined_vertical_radio, 1)
options_layout.addWidget(self.combined_vertical_radio)
vertical_hint = QLabel(f" {t('settings.layout.combinedVerticalDescription')}")
vertical_hint.setStyleSheet(separate_hint.styleSheet())
options_layout.addWidget(vertical_hint)
# 保存引用以便更新
self.ui_elements['vertical_hint'] = vertical_hint
# 合併模式(水平)
self.combined_horizontal_radio = QRadioButton(t('settings.layout.combinedHorizontal'))
self.combined_horizontal_radio.setStyleSheet(self.separate_mode_radio.styleSheet())
self.layout_button_group.addButton(self.combined_horizontal_radio, 2)
options_layout.addWidget(self.combined_horizontal_radio)
horizontal_hint = QLabel(f" {t('settings.layout.combinedHorizontalDescription')}")
horizontal_hint.setStyleSheet(separate_hint.styleSheet())
options_layout.addWidget(horizontal_hint)
# 保存引用以便更新
self.ui_elements['horizontal_hint'] = horizontal_hint
layout.addLayout(options_layout)
# 連接佈局變更信號
self.layout_button_group.buttonToggled.connect(self._on_layout_changed)
def _create_window_section(self, layout: QVBoxLayout) -> None:
"""創建視窗設置區域"""
header = self._create_section_header(t('settings.window.title'), "🖥️")
layout.addWidget(header)
# 保存引用以便更新
self.ui_elements['window_header'] = header
# 選項容器
options_layout = QVBoxLayout()
options_layout.setSpacing(8)
# 使用現代化的 Switch 組件
self.always_center_switch = SwitchWithLabel(t('settings.window.alwaysCenter'))
self.always_center_switch.setChecked(self.config_manager.get_always_center_window())
self.always_center_switch.toggled.connect(self._on_always_center_changed)
options_layout.addWidget(self.always_center_switch)
layout.addLayout(options_layout)
def _create_timeout_section(self, layout: QVBoxLayout) -> None:
"""創建超時設置區域"""
header = self._create_section_header(t('timeout.settings.title'), "")
layout.addWidget(header)
# 保存引用以便更新
self.ui_elements['timeout_header'] = header
# 選項容器
options_layout = QVBoxLayout()
options_layout.setSpacing(12)
# 啟用超時自動關閉開關
self.timeout_enabled_switch = SwitchWithLabel(t('timeout.enable'))
self.timeout_enabled_switch.setChecked(self.config_manager.get_timeout_enabled())
self.timeout_enabled_switch.toggled.connect(self._on_timeout_enabled_changed)
options_layout.addWidget(self.timeout_enabled_switch)
# 超時時間設置
timeout_duration_layout = QHBoxLayout()
timeout_duration_layout.setContentsMargins(0, 8, 0, 0)
# 標籤
timeout_duration_label = QLabel(t('timeout.duration.label'))
timeout_duration_label.setStyleSheet("""
QLabel {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
color: #ffffff;
font-size: 13px;
}
""")
timeout_duration_layout.addWidget(timeout_duration_label)
# 保存引用以便更新
self.ui_elements['timeout_duration_label'] = timeout_duration_label
# 彈性空間
timeout_duration_layout.addStretch()
# 時間輸入框
self.timeout_duration_spinbox = StyledSpinBox()
self.timeout_duration_spinbox.setRange(30, 7200) # 30秒到2小時
self.timeout_duration_spinbox.setValue(self.config_manager.get_timeout_duration())
self.timeout_duration_spinbox.setSuffix(" " + t('timeout.seconds'))
# StyledSpinBox 已經有內建樣式,不需要額外設置
self.timeout_duration_spinbox.valueChanged.connect(self._on_timeout_duration_changed)
timeout_duration_layout.addWidget(self.timeout_duration_spinbox)
options_layout.addLayout(timeout_duration_layout)
# 說明文字
description = self._create_description(t('timeout.settings.description'))
options_layout.addWidget(description)
# 保存引用以便更新
self.ui_elements['timeout_description'] = description
layout.addLayout(options_layout)
def _create_reset_section(self, layout: QVBoxLayout) -> None:
"""創建重置設定區域"""
header = self._create_section_header(t('settings.reset.title'), "🔄")
layout.addWidget(header)
# 保存引用以便更新
self.ui_elements['reset_header'] = header
reset_container = QHBoxLayout()
reset_container.setContentsMargins(0, 0, 0, 0)
self.reset_button = QPushButton(t('settings.reset.button'))
self.reset_button.setMinimumHeight(32)
self.reset_button.setMaximumWidth(110)
self.reset_button.setStyleSheet("""
QPushButton {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
padding: 6px 12px;
}
QPushButton:hover {
background-color: #e55565;
}
QPushButton:pressed {
background-color: #c82333;
}
""")
self.reset_button.clicked.connect(self._on_reset_settings)
reset_container.addWidget(self.reset_button)
reset_container.addStretch()
layout.addLayout(reset_container)
def _populate_language_selector(self) -> None:
"""填充語言選擇器"""
languages = [
('zh-TW', '繁體中文'),
('zh-CN', '简体中文'),
('en', 'English')
]
current_language = self.i18n.get_current_language()
# 暫時斷開信號連接以避免觸發變更事件
self.language_selector.blockSignals(True)
# 先清空現有選項
self.language_selector.clear()
for i, (code, name) in enumerate(languages):
self.language_selector.addItem(name, code)
if code == current_language:
self.language_selector.setCurrentIndex(i)
# 重新連接信號
self.language_selector.blockSignals(False)
def _on_language_changed(self, index: int) -> None:
"""語言變更事件處理"""
if index < 0:
return
language_code = self.language_selector.itemData(index)
if language_code and language_code != self.i18n.get_current_language():
# 先保存語言設定
self.config_manager.set_language(language_code)
# 再設定語言
self.i18n.set_language(language_code)
# 發出信號
self.language_changed.emit()
def _on_layout_changed(self, button, checked: bool) -> None:
"""佈局變更事件處理"""
if not checked:
return
button_id = self.layout_button_group.id(button)
if button_id == 0: # 分離模式
new_combined_mode = False
new_orientation = 'vertical'
elif button_id == 1: # 合併模式(垂直)
new_combined_mode = True
new_orientation = 'vertical'
elif button_id == 2: # 合併模式(水平)
new_combined_mode = True
new_orientation = 'horizontal'
else:
return
# 檢查是否真的有變更
if new_combined_mode != self.combined_mode or new_orientation != self.layout_orientation:
# 批量保存配置(避免多次寫入文件)
self.config_manager.update_partial_config({
'combined_mode': new_combined_mode,
'layout_orientation': new_orientation
})
# 更新內部狀態
self.combined_mode = new_combined_mode
self.layout_orientation = new_orientation
# 發出佈局變更請求信號
self.layout_change_requested.emit(new_combined_mode, new_orientation)
def _on_always_center_changed(self, checked: bool) -> None:
"""視窗定位選項變更事件處理"""
# 立即保存設定
self.config_manager.set_always_center_window(checked)
debug_log(f"視窗定位設置已保存: {checked}") # 調試輸出
def _on_timeout_enabled_changed(self, enabled: bool) -> None:
"""超時啟用狀態變更事件處理"""
# 立即保存設定
self.config_manager.set_timeout_enabled(enabled)
debug_log(f"超時啟用設置已保存: {enabled}")
# 發出信號通知主窗口
duration = self.timeout_duration_spinbox.value()
self.timeout_settings_changed.emit(enabled, duration)
def _on_timeout_duration_changed(self, duration: int) -> None:
"""超時時間變更事件處理"""
# 立即保存設定
self.config_manager.set_timeout_duration(duration)
debug_log(f"超時時間設置已保存: {duration}")
# 發出信號通知主窗口
enabled = self.timeout_enabled_switch.isChecked()
self.timeout_settings_changed.emit(enabled, duration)
def _on_reset_settings(self) -> None:
"""重置設定事件處理"""
reply = QMessageBox.question(
self,
t('settings.reset.confirmTitle'),
t('settings.reset.confirmMessage'),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.reset_requested.emit()
def update_texts(self) -> None:
"""更新界面文字(不重新創建界面)"""
# 更新區塊標題
if 'language_header' in self.ui_elements:
self.ui_elements['language_header'].setText(f"🌐 {t('settings.language.title')}")
if 'layout_header' in self.ui_elements:
self.ui_elements['layout_header'].setText(f"📐 {t('settings.layout.title')}")
if 'window_header' in self.ui_elements:
self.ui_elements['window_header'].setText(f"🖥️ {t('settings.window.title')}")
if 'reset_header' in self.ui_elements:
self.ui_elements['reset_header'].setText(f"🔄 {t('settings.reset.title')}")
if 'timeout_header' in self.ui_elements:
self.ui_elements['timeout_header'].setText(f"{t('timeout.settings.title')}")
# 更新提示文字
if 'separate_hint' in self.ui_elements:
self.ui_elements['separate_hint'].setText(f" {t('settings.layout.separateModeDescription')}")
if 'vertical_hint' in self.ui_elements:
self.ui_elements['vertical_hint'].setText(f" {t('settings.layout.combinedVerticalDescription')}")
if 'horizontal_hint' in self.ui_elements:
self.ui_elements['horizontal_hint'].setText(f" {t('settings.layout.combinedHorizontalDescription')}")
if 'timeout_description' in self.ui_elements:
self.ui_elements['timeout_description'].setText(t('timeout.settings.description'))
# 更新按鈕文字
if hasattr(self, 'reset_button'):
self.reset_button.setText(t('settings.reset.button'))
# 更新切換開關文字
if hasattr(self, 'always_center_switch'):
self.always_center_switch.setText(t('settings.window.alwaysCenter'))
if hasattr(self, 'timeout_enabled_switch'):
self.timeout_enabled_switch.setText(t('timeout.enable'))
# 更新超時相關標籤和控件
if 'timeout_duration_label' in self.ui_elements:
self.ui_elements['timeout_duration_label'].setText(t('timeout.duration.label'))
if hasattr(self, 'timeout_duration_spinbox'):
self.timeout_duration_spinbox.setSuffix(" " + t('timeout.seconds'))
# 更新單選按鈕文字
if hasattr(self, 'separate_mode_radio'):
self.separate_mode_radio.setText(t('settings.layout.separateMode'))
if hasattr(self, 'combined_vertical_radio'):
self.combined_vertical_radio.setText(t('settings.layout.combinedVertical'))
if hasattr(self, 'combined_horizontal_radio'):
self.combined_horizontal_radio.setText(t('settings.layout.combinedHorizontal'))
# 注意:不要重新填充語言選擇器,避免重複選項問題
def reload_settings_from_config(self) -> None:
"""從配置重新載入設定狀態"""
# 重新載入語言設定
if hasattr(self, 'language_selector'):
self._populate_language_selector()
# 重新載入佈局設定
self.combined_mode = self.config_manager.get_layout_mode()
self.layout_orientation = self.config_manager.get_layout_orientation()
self._set_initial_layout_state()
# 重新載入視窗設定
if hasattr(self, 'always_center_switch'):
always_center = self.config_manager.get_always_center_window()
self.always_center_switch.setChecked(always_center)
debug_log(f"重新載入視窗定位設置: {always_center}") # 調試輸出
# 重新載入超時設定
if hasattr(self, 'timeout_enabled_switch'):
timeout_enabled = self.config_manager.get_timeout_enabled()
self.timeout_enabled_switch.setChecked(timeout_enabled)
debug_log(f"重新載入超時啟用設置: {timeout_enabled}")
if hasattr(self, 'timeout_duration_spinbox'):
timeout_duration = self.config_manager.get_timeout_duration()
self.timeout_duration_spinbox.setValue(timeout_duration)
debug_log(f"重新載入超時時間設置: {timeout_duration}") # 調試輸出
def set_layout_mode(self, combined_mode: bool) -> None:
"""設置佈局模式"""
self.combined_mode = combined_mode
self._set_initial_layout_state()
def set_layout_orientation(self, orientation: str) -> None:
"""設置佈局方向"""
self.layout_orientation = orientation
self._set_initial_layout_state()
def _set_initial_layout_state(self) -> None:
"""設置初始佈局狀態"""
if hasattr(self, 'separate_mode_radio'):
# 暫時斷開信號連接以避免觸發變更事件
self.layout_button_group.blockSignals(True)
if not self.combined_mode:
self.separate_mode_radio.setChecked(True)
elif self.layout_orientation == 'vertical':
self.combined_vertical_radio.setChecked(True)
else:
self.combined_horizontal_radio.setChecked(True)
# 重新連接信號
self.layout_button_group.blockSignals(False)

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
摘要分頁組件
============
專門顯示AI工作摘要的分頁組件。
"""
import json
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit
from ...i18n import t
class SummaryTab(QWidget):
"""摘要分頁組件"""
def __init__(self, summary: str, parent=None):
super().__init__(parent)
self.summary = self._process_summary(summary)
self._setup_ui()
def _process_summary(self, summary: str) -> str:
"""處理摘要內容如果是JSON格式則提取實際內容"""
try:
# 嘗試解析JSON
if summary.strip().startswith('{') and summary.strip().endswith('}'):
json_data = json.loads(summary)
# 如果是JSON格式提取summary字段的內容
if isinstance(json_data, dict) and 'summary' in json_data:
return json_data['summary']
# 如果JSON中沒有summary字段返回原始內容
return summary
else:
return summary
except (json.JSONDecodeError, TypeError):
# 如果不是有效的JSON返回原始內容
return summary
def _setup_ui(self) -> None:
"""設置用戶介面"""
layout = QVBoxLayout(self)
layout.setSpacing(12)
layout.setContentsMargins(0, 16, 0, 0) # 只保留上邊距,移除左右和底部邊距
# 說明文字容器
description_wrapper = QWidget()
description_layout = QVBoxLayout(description_wrapper)
description_layout.setContentsMargins(16, 0, 16, 0) # 只對說明文字設置左右邊距
description_layout.setSpacing(0)
# 說明文字
if self._is_test_summary():
self.summary_description_label = QLabel(t('summary.testDescription'))
else:
self.summary_description_label = QLabel(t('summary.description'))
self.summary_description_label.setStyleSheet("color: #9e9e9e; font-size: 12px; margin-bottom: 10px;")
self.summary_description_label.setWordWrap(True)
description_layout.addWidget(self.summary_description_label)
layout.addWidget(description_wrapper)
# 摘要顯示區域容器
summary_wrapper = QWidget()
summary_layout = QVBoxLayout(summary_wrapper)
summary_layout.setContentsMargins(16, 0, 16, 0) # 只對摘要區域設置左右邊距
summary_layout.setSpacing(0)
# 摘要顯示區域
self.summary_display = QTextEdit()
# 檢查是否為測試摘要,如果是則使用翻譯的內容
if self._is_test_summary():
self.summary_display.setPlainText(t('test.qtGuiSummary'))
else:
self.summary_display.setPlainText(self.summary)
self.summary_display.setReadOnly(True)
self.summary_display.setStyleSheet("""
QTextEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 10px;
color: #ffffff;
font-size: 12px;
line-height: 1.4;
}
""")
summary_layout.addWidget(self.summary_display, 1)
layout.addWidget(summary_wrapper, 1)
def _is_test_summary(self) -> bool:
"""檢查是否為測試摘要"""
# 更精確的測試摘要檢測 - 必須包含特定的測試指標組合
test_patterns = [
# Qt GUI 測試特徵組合 - 必須同時包含多個特徵
("圖片預覽和視窗調整測試", "功能測試項目", "🎯"),
("圖片預覽和窗口調整測試", "功能測試項目", "🎯"),
("图片预览和窗口调整测试", "功能测试项目", "🎯"),
("Image Preview and Window Adjustment Test", "Test Items", "🎯"),
# Web UI 測試特徵組合
("測試 Web UI 功能", "🎯 **功能測試項目", "WebSocket 即時通訊"),
("测试 Web UI 功能", "🎯 **功能测试项目", "WebSocket 即时通讯"),
("Test Web UI Functionality", "🎯 **Test Items", "WebSocket real-time communication"),
# 具體的測試步驟特徵
("智能 Ctrl+V 圖片貼上功能", "📋 測試步驟", "請測試這些功能並提供回饋"),
("智能 Ctrl+V 图片粘贴功能", "📋 测试步骤", "请测试这些功能并提供回馈"),
("Smart Ctrl+V image paste", "📋 Test Steps", "Please test these features"),
]
# 檢查是否匹配任何一個測試模式(必須同時包含模式中的所有關鍵詞)
for pattern in test_patterns:
if all(keyword in self.summary for keyword in pattern):
return True
return False
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
if self._is_test_summary():
self.summary_description_label.setText(t('summary.testDescription'))
# 更新測試摘要的內容
self.summary_display.setPlainText(t('test.qtGuiSummary'))
else:
self.summary_description_label.setText(t('summary.description'))

View File

@@ -0,0 +1,14 @@
"""
GUI 工具函數模組
===============
包含各種輔助工具函數。
"""
from .shortcuts import setup_shortcuts
from .utils import apply_widget_styles
__all__ = [
'setup_shortcuts',
'apply_widget_styles'
]

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快捷鍵設置工具
==============
管理 GUI 快捷鍵設置的工具函數。
"""
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtCore import Qt
def setup_shortcuts(window):
"""
設置窗口的快捷鍵
Args:
window: 主窗口實例
"""
# Ctrl+Enter 提交回饋
submit_shortcut = QShortcut(QKeySequence("Ctrl+Return"), window)
submit_shortcut.activated.connect(window._submit_feedback)
# Escape 取消回饋
cancel_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), window)
cancel_shortcut.activated.connect(window._cancel_feedback)
# Ctrl+R 執行命令
run_shortcut = QShortcut(QKeySequence("Ctrl+R"), window)
run_shortcut.activated.connect(window._run_command)
# Ctrl+Shift+C 終止命令
terminate_shortcut = QShortcut(QKeySequence("Ctrl+Shift+C"), window)
terminate_shortcut.activated.connect(window._terminate_command)

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通用工具函數
============
提供 GUI 相關的通用工具函數。
"""
from ..styles import *
def apply_widget_styles(widget, style_type="default"):
"""
應用樣式到元件
Args:
widget: 要應用樣式的元件
style_type: 樣式類型
"""
if style_type == "primary_button":
widget.setStyleSheet(PRIMARY_BUTTON_STYLE)
elif style_type == "success_button":
widget.setStyleSheet(SUCCESS_BUTTON_STYLE)
elif style_type == "danger_button":
widget.setStyleSheet(DANGER_BUTTON_STYLE)
elif style_type == "secondary_button":
widget.setStyleSheet(SECONDARY_BUTTON_STYLE)
elif style_type == "dark_theme":
widget.setStyleSheet(DARK_STYLE)
def format_file_size(size_bytes):
"""
格式化文件大小顯示
Args:
size_bytes: 文件大小(字節)
Returns:
str: 格式化後的文件大小字符串
"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
size_kb = size_bytes / 1024
return f"{size_kb:.1f} KB"
else:
size_mb = size_bytes / (1024 * 1024)
return f"{size_mb:.1f} MB"

View File

@@ -0,0 +1,19 @@
"""
GUI 自定義元件模組
==================
包含所有自定義的 GUI 元件。
"""
from .text_edit import SmartTextEdit
from .image_preview import ImagePreviewWidget
from .image_upload import ImageUploadWidget
from .switch import SwitchWidget, SwitchWithLabel
__all__ = [
'SmartTextEdit',
'ImagePreviewWidget',
'ImageUploadWidget',
'SwitchWidget',
'SwitchWithLabel'
]

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
圖片預覽元件
============
提供圖片預覽和刪除功能的自定義元件。
"""
import os
from PySide6.QtWidgets import QLabel, QPushButton, QFrame, QMessageBox
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QPixmap
# 導入多語系支援
from ...i18n import t
class ImagePreviewWidget(QLabel):
"""圖片預覽元件"""
remove_clicked = Signal(str)
def __init__(self, image_path: str, image_id: str, parent=None):
super().__init__(parent)
self.image_path = image_path
self.image_id = image_id
self._setup_widget()
self._load_image()
self._create_delete_button()
def _setup_widget(self) -> None:
"""設置元件基本屬性"""
self.setFixedSize(100, 100)
self.setFrameStyle(QFrame.Box)
self.setStyleSheet("""
QLabel {
border: 2px solid #464647;
border-radius: 8px;
background-color: #2d2d30;
padding: 2px;
}
QLabel:hover {
border-color: #007acc;
background-color: #383838;
}
""")
self.setToolTip(f"圖片: {os.path.basename(self.image_path)}")
def _load_image(self) -> None:
"""載入並顯示圖片"""
try:
pixmap = QPixmap(self.image_path)
if not pixmap.isNull():
scaled_pixmap = pixmap.scaled(96, 96, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.setPixmap(scaled_pixmap)
self.setAlignment(Qt.AlignCenter)
else:
self.setText("無法載入圖片")
self.setAlignment(Qt.AlignCenter)
except Exception:
self.setText("載入錯誤")
self.setAlignment(Qt.AlignCenter)
def _create_delete_button(self) -> None:
"""創建刪除按鈕"""
self.delete_button = QPushButton("×", self)
self.delete_button.setFixedSize(20, 20)
self.delete_button.move(78, 2)
self.delete_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: #ffffff;
border: none;
border-radius: 10px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
""")
self.delete_button.clicked.connect(self._on_delete_clicked)
self.delete_button.setToolTip(t('images.clear'))
def _on_delete_clicked(self) -> None:
"""處理刪除按鈕點擊事件"""
reply = QMessageBox.question(
self, t('images.deleteTitle'),
t('images.deleteConfirm', filename=os.path.basename(self.image_path)),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.remove_clicked.emit(self.image_id)

View File

@@ -0,0 +1,740 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
圖片上傳元件
============
支援文件選擇、剪貼板貼上、拖拽上傳等多種方式的圖片上傳元件。
"""
import os
import uuid
import time
from typing import Dict, List
from pathlib import Path
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QScrollArea, QGridLayout, QFileDialog, QMessageBox, QApplication,
QComboBox, QCheckBox, QGroupBox, QFrame
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent
from PySide6.QtWidgets import QSizePolicy
# 導入多語系支援
from ...i18n import t
from ...debug import gui_debug_log as debug_log
from .image_preview import ImagePreviewWidget
class ImageUploadWidget(QWidget):
"""圖片上傳元件"""
images_changed = Signal()
def __init__(self, parent=None, config_manager=None):
super().__init__(parent)
self.images: Dict[str, Dict[str, str]] = {}
self.config_manager = config_manager
self._last_paste_time = 0 # 添加最後貼上時間記錄
self._setup_ui()
self.setAcceptDrops(True)
# 啟動時清理舊的臨時文件
self._cleanup_old_temp_files()
def _setup_ui(self) -> None:
"""設置用戶介面"""
layout = QVBoxLayout(self)
layout.setSpacing(6)
layout.setContentsMargins(0, 8, 0, 8) # 調整邊距使其與其他區域一致
# 標題
self.title = QLabel(t('images.title'))
self.title.setFont(QFont("", 10, QFont.Bold))
self.title.setStyleSheet("color: #007acc; margin: 1px 0;")
layout.addWidget(self.title)
# 圖片設定區域
self._create_settings_area(layout)
# 狀態標籤
self.status_label = QLabel(t('images.status', count=0))
self.status_label.setStyleSheet("color: #9e9e9e; font-size: 10px; margin: 5px 0;")
layout.addWidget(self.status_label)
# 統一的圖片區域(整合按鈕、拖拽、預覽)
self._create_unified_image_area(layout)
def _create_settings_area(self, layout: QVBoxLayout) -> None:
"""創建圖片設定區域"""
if not self.config_manager:
return # 如果沒有 config_manager跳過設定區域
# 設定群組框
settings_group = QGroupBox(t('images.settings.title'))
settings_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
font-size: 9px;
color: #9e9e9e;
border: 1px solid #464647;
border-radius: 4px;
margin-top: 6px;
padding-top: 4px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 8px;
padding: 0 4px 0 4px;
}
""")
settings_layout = QHBoxLayout(settings_group)
settings_layout.setSpacing(12)
settings_layout.setContentsMargins(8, 8, 8, 8)
# 圖片大小限制設定
self.size_label = QLabel(t('images.settings.sizeLimit') + ":")
self.size_label.setStyleSheet("color: #cccccc; font-size: 11px;")
self.size_limit_combo = QComboBox()
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.unlimited'), 0)
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.1mb'), 1024*1024)
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.3mb'), 3*1024*1024)
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.5mb'), 5*1024*1024)
# 載入當前設定
current_limit = self.config_manager.get_image_size_limit()
for i in range(self.size_limit_combo.count()):
if self.size_limit_combo.itemData(i) == current_limit:
self.size_limit_combo.setCurrentIndex(i)
break
self.size_limit_combo.currentIndexChanged.connect(self._on_size_limit_changed)
# Base64 詳細模式設定
self.base64_checkbox = QCheckBox(t('images.settings.base64Detail'))
self.base64_checkbox.setChecked(self.config_manager.get_enable_base64_detail())
self.base64_checkbox.stateChanged.connect(self._on_base64_detail_changed)
self.base64_checkbox.setToolTip(t('images.settings.base64DetailHelp'))
# Base64 警告標籤
self.base64_warning = QLabel(t('images.settings.base64Warning'))
self.base64_warning.setStyleSheet("color: #ff9800; font-size: 10px;")
# 添加到佈局
settings_layout.addWidget(self.size_label)
settings_layout.addWidget(self.size_limit_combo)
settings_layout.addWidget(self.base64_checkbox)
settings_layout.addWidget(self.base64_warning)
settings_layout.addStretch()
layout.addWidget(settings_group)
def _on_size_limit_changed(self, index: int) -> None:
"""圖片大小限制變更處理"""
if self.config_manager and index >= 0:
size_bytes = self.size_limit_combo.itemData(index)
# 處理 None 值
if size_bytes is not None:
self.config_manager.set_image_size_limit(size_bytes)
debug_log(f"圖片大小限制已更新: {size_bytes} bytes")
def _on_base64_detail_changed(self, state: int) -> None:
"""Base64 詳細模式變更處理"""
if self.config_manager:
enabled = state == Qt.Checked
self.config_manager.set_enable_base64_detail(enabled)
debug_log(f"Base64 詳細模式已更新: {enabled}")
def _create_unified_image_area(self, layout: QVBoxLayout) -> None:
"""創建統一的圖片區域"""
# 創建滾動區域
self.preview_scroll = QScrollArea()
self.preview_widget = QWidget()
self.preview_widget.setMinimumHeight(140) # 設置預覽部件的最小高度
# 設置尺寸策略,允許垂直擴展
self.preview_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
self.preview_layout = QVBoxLayout(self.preview_widget)
self.preview_layout.setSpacing(6)
self.preview_layout.setContentsMargins(8, 8, 8, 8)
# 創建操作按鈕區域
self._create_buttons_in_area()
# 創建拖拽提示標籤(初始顯示)
self.drop_hint_label = QLabel(t('images.dragHint'))
self.drop_hint_label.setAlignment(Qt.AlignCenter)
self.drop_hint_label.setMinimumHeight(80) # 增加最小高度
self.drop_hint_label.setMaximumHeight(120) # 設置最大高度
self.drop_hint_label.setStyleSheet("""
QLabel {
border: 2px dashed #464647;
border-radius: 6px;
background-color: #2d2d30;
color: #9e9e9e;
font-size: 11px;
margin: 4px 0;
padding: 16px 8px;
}
""")
# 創建圖片網格容器
self.images_grid_widget = QWidget()
self.images_grid_layout = QGridLayout(self.images_grid_widget)
self.images_grid_layout.setSpacing(4)
self.images_grid_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
# 將部分添加到主布局
self.preview_layout.addWidget(self.button_widget) # 按鈕始終顯示
self.preview_layout.addWidget(self.drop_hint_label)
self.preview_layout.addWidget(self.images_grid_widget)
# 初始時隱藏圖片網格
self.images_grid_widget.hide()
# 設置滾動區域
self.preview_scroll.setWidget(self.preview_widget)
self.preview_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # 改回按需顯示滾動條
self.preview_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.preview_scroll.setMinimumHeight(160) # 增加最小高度,確保有足夠空間
self.preview_scroll.setMaximumHeight(300) # 增加最大高度
self.preview_scroll.setWidgetResizable(True)
# 增強的滾動區域樣式,改善 macOS 兼容性
self.preview_scroll.setStyleSheet("""
QScrollArea {
border: 1px solid #464647;
border-radius: 4px;
background-color: #1e1e1e;
}
QScrollArea QScrollBar:vertical {
background-color: #2a2a2a;
width: 14px;
border-radius: 7px;
margin: 0;
}
QScrollArea QScrollBar::handle:vertical {
background-color: #555;
border-radius: 7px;
min-height: 30px;
margin: 2px;
}
QScrollArea QScrollBar::handle:vertical:hover {
background-color: #777;
}
QScrollArea QScrollBar::add-line:vertical,
QScrollArea QScrollBar::sub-line:vertical {
border: none;
background: none;
height: 0px;
}
QScrollArea QScrollBar:horizontal {
background-color: #2a2a2a;
height: 14px;
border-radius: 7px;
margin: 0;
}
QScrollArea QScrollBar::handle:horizontal {
background-color: #555;
border-radius: 7px;
min-width: 30px;
margin: 2px;
}
QScrollArea QScrollBar::handle:horizontal:hover {
background-color: #777;
}
QScrollArea QScrollBar::add-line:horizontal,
QScrollArea QScrollBar::sub-line:horizontal {
border: none;
background: none;
width: 0px;
}
""")
layout.addWidget(self.preview_scroll)
def _create_buttons_in_area(self) -> None:
"""在統一區域內創建操作按鈕"""
self.button_widget = QWidget()
button_layout = QHBoxLayout(self.button_widget)
button_layout.setContentsMargins(0, 0, 0, 4)
button_layout.setSpacing(6)
# 選擇文件按鈕
self.file_button = QPushButton(t('buttons.selectFiles'))
self.file_button.clicked.connect(self.select_files)
# 剪貼板按鈕
self.paste_button = QPushButton(t('buttons.pasteClipboard'))
self.paste_button.clicked.connect(self.paste_from_clipboard)
# 清除按鈕
self.clear_button = QPushButton(t('buttons.clearAll'))
self.clear_button.clicked.connect(self.clear_all_images)
# 設置按鈕樣式(更緊湊)
button_style = """
QPushButton {
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-weight: bold;
font-size: 10px;
min-height: 24px;
}
QPushButton:hover {
opacity: 0.8;
}
"""
self.file_button.setStyleSheet(button_style + """
QPushButton {
background-color: #0e639c;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.paste_button.setStyleSheet(button_style + """
QPushButton {
background-color: #4caf50;
}
QPushButton:hover {
background-color: #45a049;
}
""")
self.clear_button.setStyleSheet(button_style + """
QPushButton {
background-color: #f44336;
color: #ffffff;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
""")
button_layout.addWidget(self.file_button)
button_layout.addWidget(self.paste_button)
button_layout.addWidget(self.clear_button)
button_layout.addStretch() # 左對齊按鈕
def select_files(self) -> None:
"""選擇文件對話框"""
files, _ = QFileDialog.getOpenFileNames(
self,
t('images.select'),
"",
"Image files (*.png *.jpg *.jpeg *.gif *.bmp *.webp);;All files (*)"
)
if files:
self._add_images(files)
def paste_from_clipboard(self) -> None:
"""從剪貼板粘貼圖片"""
# 防重複保護:檢查是否在短時間內重複調用
current_time = time.time() * 1000 # 轉換為毫秒
if current_time - self._last_paste_time < 100: # 100毫秒內的重複調用忽略
debug_log(f"忽略重複的剪貼板粘貼請求(間隔: {current_time - self._last_paste_time:.1f}ms")
return
self._last_paste_time = current_time
clipboard = QApplication.clipboard()
mimeData = clipboard.mimeData()
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"
# 保存剪貼板圖片
if image.save(str(temp_file), "PNG"):
if os.path.getsize(temp_file) > 0:
self._add_images([str(temp_file)])
debug_log(f"從剪貼板成功粘貼圖片: {temp_file}")
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.imageSaveEmpty', path=str(temp_file)))
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.imageSaveFailed'))
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.clipboardSaveFailed'))
elif mimeData.hasText():
# 檢查是否為圖片數據
text = mimeData.text()
if text.startswith('data:image/') or any(ext in text.lower() for ext in ['.png', '.jpg', '.jpeg', '.gif']):
QMessageBox.information(self, t('errors.info'), t('errors.noValidImage'))
else:
QMessageBox.information(self, t('errors.info'), t('errors.noImageContent'))
def clear_all_images(self) -> None:
"""清除所有圖片"""
if self.images:
reply = QMessageBox.question(
self, t('errors.confirmClearTitle'),
t('errors.confirmClearAll', count=len(self.images)),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
# 清理臨時文件
temp_files_cleaned = 0
for image_info in self.images.values():
file_path = image_info["path"]
if "clipboard_" in os.path.basename(file_path) and ".cache" in file_path:
try:
if os.path.exists(file_path):
os.remove(file_path)
temp_files_cleaned += 1
debug_log(f"已刪除臨時文件: {file_path}")
except Exception as e:
debug_log(f"刪除臨時文件失敗: {e}")
# 清除內存中的圖片數據
self.images.clear()
self._refresh_preview()
self._update_status()
self.images_changed.emit()
debug_log(f"已清除所有圖片,包括 {temp_files_cleaned} 個臨時文件")
def _add_images(self, file_paths: List[str]) -> None:
"""添加圖片"""
added_count = 0
for file_path in file_paths:
try:
debug_log(f"嘗試添加圖片: {file_path}")
if not os.path.exists(file_path):
debug_log(f"文件不存在: {file_path}")
continue
if not self._is_image_file(file_path):
debug_log(f"不是圖片文件: {file_path}")
continue
file_size = os.path.getsize(file_path)
debug_log(f"文件大小: {file_size} bytes")
# 動態圖片大小限制檢查
size_limit = self.config_manager.get_image_size_limit() if self.config_manager else 1024*1024
if size_limit > 0 and file_size > size_limit:
# 格式化限制大小顯示
if size_limit >= 1024*1024:
limit_str = f"{size_limit/(1024*1024):.0f}MB"
else:
limit_str = f"{size_limit/1024:.0f}KB"
# 格式化文件大小顯示
if file_size >= 1024*1024:
size_str = f"{file_size/(1024*1024):.1f}MB"
else:
size_str = f"{file_size/1024:.1f}KB"
QMessageBox.warning(
self, t('errors.warning'),
t('images.sizeLimitExceeded', filename=os.path.basename(file_path), size=size_str, limit=limit_str) +
"\n\n" + t('images.sizeLimitExceededAdvice')
)
continue
if file_size == 0:
QMessageBox.warning(self, t('errors.warning'), t('errors.emptyFile', filename=os.path.basename(file_path)))
continue
# 讀取圖片原始二進制數據
with open(file_path, 'rb') as f:
raw_data = f.read()
debug_log(f"讀取原始數據大小: {len(raw_data)} bytes")
if len(raw_data) == 0:
debug_log(f"讀取的數據為空!")
continue
# 再次檢查內存中的數據大小(使用配置的限制)
size_limit = self.config_manager.get_image_size_limit() if self.config_manager else 1024*1024
if size_limit > 0 and len(raw_data) > size_limit:
# 格式化限制大小顯示
if size_limit >= 1024*1024:
limit_str = f"{size_limit/(1024*1024):.0f}MB"
else:
limit_str = f"{size_limit/1024:.0f}KB"
# 格式化文件大小顯示
if len(raw_data) >= 1024*1024:
size_str = f"{len(raw_data)/(1024*1024):.1f}MB"
else:
size_str = f"{len(raw_data)/1024:.1f}KB"
QMessageBox.warning(
self, t('errors.warning'),
t('images.sizeLimitExceeded', filename=os.path.basename(file_path), size=size_str, limit=limit_str) +
"\n\n" + t('images.sizeLimitExceededAdvice')
)
continue
image_id = str(uuid.uuid4())
self.images[image_id] = {
"path": file_path,
"data": raw_data, # 直接保存原始二進制數據
"name": os.path.basename(file_path),
"size": file_size
}
added_count += 1
debug_log(f"圖片添加成功: {os.path.basename(file_path)}")
except Exception as e:
debug_log(f"添加圖片失敗: {e}")
QMessageBox.warning(self, t('errors.title'), t('errors.loadImageFailed', filename=os.path.basename(file_path), error=str(e)))
if added_count > 0:
debug_log(f"共添加 {added_count} 張圖片,當前總數: {len(self.images)}")
self._refresh_preview()
self._update_status()
self.images_changed.emit()
def _is_image_file(self, file_path: str) -> bool:
"""檢查是否為支援的圖片格式"""
extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
return Path(file_path).suffix.lower() in extensions
def _refresh_preview(self) -> None:
"""刷新預覽布局"""
# 清除現有預覽
while self.images_grid_layout.count():
child = self.images_grid_layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
# 根據圖片數量決定顯示內容
if len(self.images) == 0:
# 沒有圖片時,顯示拖拽提示
self.drop_hint_label.show()
self.images_grid_widget.hide()
else:
# 有圖片時,隱藏拖拽提示,顯示圖片網格
self.drop_hint_label.hide()
self.images_grid_widget.show()
# 重新添加圖片預覽
for i, (image_id, image_info) in enumerate(self.images.items()):
preview = ImagePreviewWidget(image_info["path"], image_id, self)
preview.remove_clicked.connect(self._remove_image)
row = i // 5
col = i % 5
self.images_grid_layout.addWidget(preview, row, col)
# 強制更新佈局和滾動區域
self.preview_widget.updateGeometry()
self.preview_scroll.updateGeometry()
# 確保滾動區域能正確計算內容大小
from PySide6.QtWidgets import QApplication
QApplication.processEvents()
def _remove_image(self, image_id: str) -> None:
"""移除圖片"""
if image_id in self.images:
image_info = self.images[image_id]
# 如果是臨時文件(剪貼板圖片),則物理刪除文件
file_path = image_info["path"]
if "clipboard_" in os.path.basename(file_path) and ".cache" in file_path:
try:
if os.path.exists(file_path):
os.remove(file_path)
debug_log(f"已刪除臨時文件: {file_path}")
except Exception as e:
debug_log(f"刪除臨時文件失敗: {e}")
# 從內存中移除圖片數據
del self.images[image_id]
self._refresh_preview()
self._update_status()
self.images_changed.emit()
debug_log(f"已移除圖片: {image_info['name']}")
def _update_status(self) -> None:
"""更新狀態標籤"""
count = len(self.images)
if count == 0:
self.status_label.setText(t('images.status', count=0))
else:
total_size = sum(img["size"] for img in self.images.values())
# 格式化文件大小
if total_size > 1024 * 1024: # MB
size_mb = total_size / (1024 * 1024)
size_str = f"{size_mb:.1f} MB"
else: # KB
size_kb = total_size / 1024
size_str = f"{size_kb:.1f} KB"
self.status_label.setText(t('images.statusWithSize', count=count, size=size_str))
# 基本調試信息
debug_log(f"圖片狀態: {count} 張圖片,總大小: {size_str}")
def get_images_data(self) -> List[dict]:
"""獲取所有圖片的數據列表"""
images_data = []
for image_info in self.images.values():
images_data.append(image_info)
return images_data
def add_image_data(self, image_data: dict) -> None:
"""添加圖片數據(用於恢復界面時的圖片)"""
try:
# 檢查必要的字段
if not all(key in image_data for key in ['filename', 'data', 'size']):
debug_log("圖片數據格式不正確,缺少必要字段")
return
# 生成新的圖片ID
image_id = str(uuid.uuid4())
# 復制圖片數據
self.images[image_id] = image_data.copy()
# 刷新預覽
self._refresh_preview()
self._update_status()
self.images_changed.emit()
debug_log(f"成功恢復圖片: {image_data['filename']}")
except Exception as e:
debug_log(f"添加圖片數據失敗: {e}")
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
"""拖拽進入事件"""
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
if url.isLocalFile() and self._is_image_file(url.toLocalFile()):
event.acceptProposedAction()
self.drop_hint_label.setStyleSheet("""
QLabel {
border: 2px dashed #007acc;
border-radius: 6px;
background-color: #383838;
color: #007acc;
font-size: 11px;
}
""")
return
event.ignore()
def dragLeaveEvent(self, event) -> None:
"""拖拽離開事件"""
self.drop_hint_label.setStyleSheet("""
QLabel {
border: 2px dashed #464647;
border-radius: 6px;
background-color: #2d2d30;
color: #9e9e9e;
font-size: 11px;
}
""")
def dropEvent(self, event: QDropEvent) -> None:
"""拖拽放下事件"""
self.dragLeaveEvent(event)
files = []
for url in event.mimeData().urls():
if url.isLocalFile():
file_path = url.toLocalFile()
if self._is_image_file(file_path):
files.append(file_path)
if files:
self._add_images(files)
event.acceptProposedAction()
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.dragInvalidFiles'))
def _cleanup_old_temp_files(self) -> None:
"""清理舊的臨時文件"""
try:
temp_dir = Path.home() / ".cache" / "interactive-feedback-mcp"
if temp_dir.exists():
cleaned_count = 0
for temp_file in temp_dir.glob("clipboard_*.png"):
try:
# 清理超過1小時的臨時文件
if temp_file.exists():
file_age = time.time() - temp_file.stat().st_mtime
if file_age > 3600: # 1小時 = 3600秒
temp_file.unlink()
cleaned_count += 1
except Exception as e:
debug_log(f"清理舊臨時文件失敗: {e}")
if cleaned_count > 0:
debug_log(f"清理了 {cleaned_count} 個舊的臨時文件")
except Exception as e:
debug_log(f"臨時文件清理過程出錯: {e}")
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
# 更新標題
if hasattr(self, 'title'):
self.title.setText(t('images.title'))
# 更新設定區域標籤
if hasattr(self, 'size_label'):
self.size_label.setText(t('images.settings.sizeLimit') + ":")
if hasattr(self, 'base64_warning'):
self.base64_warning.setText(t('images.settings.base64Warning'))
# 更新設定區域文字
if hasattr(self, 'size_limit_combo'):
# 保存當前選擇
current_data = self.size_limit_combo.currentData()
# 暫時斷開信號連接以避免觸發變更事件
self.size_limit_combo.blockSignals(True)
# 清除並重新添加選項
self.size_limit_combo.clear()
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.unlimited'), 0)
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.1mb'), 1024*1024)
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.3mb'), 3*1024*1024)
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.5mb'), 5*1024*1024)
# 恢復選擇
for i in range(self.size_limit_combo.count()):
if self.size_limit_combo.itemData(i) == current_data:
self.size_limit_combo.setCurrentIndex(i)
break
# 重新連接信號
self.size_limit_combo.blockSignals(False)
if hasattr(self, 'base64_checkbox'):
self.base64_checkbox.setText(t('images.settings.base64Detail'))
self.base64_checkbox.setToolTip(t('images.settings.base64DetailHelp'))
# 更新按鈕文字
if hasattr(self, 'file_button'):
self.file_button.setText(t('buttons.selectFiles'))
if hasattr(self, 'paste_button'):
self.paste_button.setText(t('buttons.pasteClipboard'))
if hasattr(self, 'clear_button'):
self.clear_button.setText(t('buttons.clearAll'))
# 更新拖拽區域文字
if hasattr(self, 'drop_hint_label'):
self.drop_hint_label.setText(t('images.dragHint'))
# 更新狀態文字
self._update_status()

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
自定義樣式的 QSpinBox
==================
提供美觀的深色主題 QSpinBox帶有自定義箭頭按鈕。
"""
from PySide6.QtWidgets import QSpinBox, QStyleOptionSpinBox, QStyle
from PySide6.QtCore import QRect, Qt
from PySide6.QtGui import QPainter, QPen, QBrush, QColor
class StyledSpinBox(QSpinBox):
"""自定義樣式的 QSpinBox"""
def __init__(self, parent=None):
super().__init__(parent)
self._setup_style()
def _setup_style(self):
"""設置基本樣式"""
self.setStyleSheet("""
QSpinBox {
background-color: #3c3c3c;
border: 1px solid #555555;
border-radius: 6px;
padding: 4px 8px;
color: #ffffff;
font-size: 12px;
min-width: 100px;
min-height: 24px;
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
}
QSpinBox:focus {
border-color: #007acc;
background-color: #404040;
}
QSpinBox:hover {
background-color: #404040;
border-color: #666666;
}
QSpinBox::up-button {
subcontrol-origin: border;
subcontrol-position: top right;
width: 20px;
border-left: 1px solid #555555;
border-bottom: 1px solid #555555;
border-top-right-radius: 5px;
background-color: #4a4a4a;
}
QSpinBox::up-button:hover {
background-color: #5a5a5a;
}
QSpinBox::up-button:pressed {
background-color: #007acc;
}
QSpinBox::down-button {
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 20px;
border-left: 1px solid #555555;
border-top: 1px solid #555555;
border-bottom-right-radius: 5px;
background-color: #4a4a4a;
}
QSpinBox::down-button:hover {
background-color: #5a5a5a;
}
QSpinBox::down-button:pressed {
background-color: #007acc;
}
QSpinBox::up-arrow {
width: 0px;
height: 0px;
}
QSpinBox::down-arrow {
width: 0px;
height: 0px;
}
""")
def paintEvent(self, event):
"""重寫繪製事件以添加自定義箭頭"""
# 先調用父類的繪製方法
super().paintEvent(event)
# 創建畫筆
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 獲取按鈕區域
opt = QStyleOptionSpinBox()
self.initStyleOption(opt)
# 計算按鈕位置
button_width = 20
widget_rect = self.rect()
# 上箭頭按鈕區域
up_rect = QRect(
widget_rect.width() - button_width,
1,
button_width - 1,
widget_rect.height() // 2 - 1
)
# 下箭頭按鈕區域
down_rect = QRect(
widget_rect.width() - button_width,
widget_rect.height() // 2,
button_width - 1,
widget_rect.height() // 2 - 1
)
# 繪製上箭頭
self._draw_arrow(painter, up_rect, True)
# 繪製下箭頭
self._draw_arrow(painter, down_rect, False)
def _draw_arrow(self, painter: QPainter, rect: QRect, is_up: bool):
"""繪製箭頭"""
# 設置畫筆
pen = QPen(QColor("#cccccc"), 1)
painter.setPen(pen)
painter.setBrush(QBrush(QColor("#cccccc")))
# 計算箭頭位置
center_x = rect.center().x()
center_y = rect.center().y()
arrow_size = 4
if is_up:
# 上箭頭:▲
points = [
(center_x, center_y - arrow_size // 2), # 頂點
(center_x - arrow_size, center_y + arrow_size // 2), # 左下
(center_x + arrow_size, center_y + arrow_size // 2) # 右下
]
else:
# 下箭頭:▼
points = [
(center_x, center_y + arrow_size // 2), # 底點
(center_x - arrow_size, center_y - arrow_size // 2), # 左上
(center_x + arrow_size, center_y - arrow_size // 2) # 右上
]
# 繪製三角形
from PySide6.QtCore import QPoint
triangle = [QPoint(x, y) for x, y in points]
painter.drawPolygon(triangle)

View File

@@ -0,0 +1,237 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
現代化切換開關組件
==================
提供類似 web 的現代化 switch 切換開關。
"""
from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QToolTip
from PySide6.QtCore import Signal, QPropertyAnimation, QRect, QEasingCurve, Property, Qt, QTimer
from PySide6.QtGui import QPainter, QColor, QPainterPath, QFont
class SwitchWidget(QWidget):
"""現代化切換開關組件"""
toggled = Signal(bool) # 狀態變更信號
def __init__(self, parent=None):
super().__init__(parent)
# 狀態變數
self._checked = False
self._enabled = True
self._animating = False
# 尺寸設定
self._width = 50
self._height = 24
self._thumb_radius = 10
self._track_radius = 12
# 顏色設定
self._track_color_off = QColor(102, 102, 102) # #666666
self._track_color_on = QColor(0, 120, 212) # #0078d4
self._thumb_color = QColor(255, 255, 255) # white
self._track_color_disabled = QColor(68, 68, 68) # #444444
# 動畫屬性
self._thumb_position = 2.0
# 設定基本屬性
self.setFixedSize(self._width, self._height)
self.setCursor(Qt.PointingHandCursor)
# 創建動畫
self._animation = QPropertyAnimation(self, b"thumbPosition")
self._animation.setDuration(200) # 200ms 動畫時間
self._animation.setEasingCurve(QEasingCurve.OutCubic)
# 設置工具提示延遲
self._tooltip_timer = QTimer()
self._tooltip_timer.setSingleShot(True)
self._tooltip_timer.timeout.connect(self._show_delayed_tooltip)
@Property(float)
def thumbPosition(self):
return self._thumb_position
@thumbPosition.setter
def thumbPosition(self, position):
self._thumb_position = position
self.update()
def isChecked(self) -> bool:
"""獲取選中狀態"""
return self._checked
def setChecked(self, checked: bool) -> None:
"""設置選中狀態"""
if self._checked != checked:
self._checked = checked
self._animate_to_position()
self.toggled.emit(checked)
def setEnabled(self, enabled: bool) -> None:
"""設置啟用狀態"""
super().setEnabled(enabled)
self._enabled = enabled
self.setCursor(Qt.PointingHandCursor if enabled else Qt.ArrowCursor)
self.update()
def _animate_to_position(self) -> None:
"""動畫到目標位置"""
if self._animating:
return
self._animating = True
target_position = self._width - self._thumb_radius * 2 - 2 if self._checked else 2
self._animation.setStartValue(self._thumb_position)
self._animation.setEndValue(target_position)
self._animation.finished.connect(self._on_animation_finished)
self._animation.start()
def _on_animation_finished(self) -> None:
"""動畫完成處理"""
self._animating = False
self._animation.finished.disconnect()
def mousePressEvent(self, event):
"""滑鼠按下事件"""
if event.button() == Qt.LeftButton and self._enabled:
self.setChecked(not self._checked)
super().mousePressEvent(event)
def enterEvent(self, event):
"""滑鼠進入事件"""
if self._enabled:
# 延遲顯示工具提示
self._tooltip_timer.start(500) # 500ms 延遲
super().enterEvent(event)
def leaveEvent(self, event):
"""滑鼠離開事件"""
self._tooltip_timer.stop()
super().leaveEvent(event)
def _show_delayed_tooltip(self):
"""顯示延遲的工具提示"""
if self.toolTip():
QToolTip.showText(self.mapToGlobal(self.rect().center()), self.toolTip(), self)
def paintEvent(self, event):
"""繪製事件"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 計算軌道矩形
track_rect = QRect(0, (self._height - self._track_radius * 2) // 2,
self._width, self._track_radius * 2)
# 繪製軌道
track_path = QPainterPath()
track_path.addRoundedRect(track_rect, self._track_radius, self._track_radius)
if not self._enabled:
track_color = self._track_color_disabled
elif self._checked:
track_color = self._track_color_on
else:
track_color = self._track_color_off
painter.fillPath(track_path, track_color)
# 繪製滑塊
thumb_x = self._thumb_position
thumb_y = (self._height - self._thumb_radius * 2) // 2
thumb_rect = QRect(int(thumb_x), thumb_y, self._thumb_radius * 2, self._thumb_radius * 2)
thumb_path = QPainterPath()
thumb_path.addEllipse(thumb_rect)
# 滑塊顏色(可以根據狀態調整透明度)
thumb_color = self._thumb_color
if not self._enabled:
thumb_color.setAlpha(180) # 半透明效果
painter.fillPath(thumb_path, thumb_color)
# 添加微妙的陰影效果
if self._enabled:
shadow_color = QColor(0, 0, 0, 30)
shadow_rect = thumb_rect.translated(0, 1)
shadow_path = QPainterPath()
shadow_path.addEllipse(shadow_rect)
painter.fillPath(shadow_path, shadow_color)
class SwitchWithLabel(QWidget):
"""帶標籤的切換開關組件"""
toggled = Signal(bool)
def __init__(self, text: str = "", parent=None):
super().__init__(parent)
# 創建布局
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
# 創建標籤
self.label = QLabel(text)
self.label.setStyleSheet("""
QLabel {
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
font-size: 13px;
color: #ffffff;
}
""")
# 創建切換開關
self.switch = SwitchWidget()
self.switch.toggled.connect(self.toggled.emit)
# 添加到布局
layout.addWidget(self.label)
layout.addStretch() # 彈性空間,將開關推到右側
layout.addWidget(self.switch)
# 設置點擊標籤也能切換開關
self.label.mousePressEvent = self._on_label_clicked
def _on_label_clicked(self, event):
"""標籤點擊事件"""
if event.button() == Qt.LeftButton:
self.switch.setChecked(not self.switch.isChecked())
def setText(self, text: str) -> None:
"""設置標籤文字"""
self.label.setText(text)
def text(self) -> str:
"""獲取標籤文字"""
return self.label.text()
def isChecked(self) -> bool:
"""獲取選中狀態"""
return self.switch.isChecked()
def setChecked(self, checked: bool) -> None:
"""設置選中狀態"""
self.switch.setChecked(checked)
def setEnabled(self, enabled: bool) -> None:
"""設置啟用狀態"""
super().setEnabled(enabled)
self.switch.setEnabled(enabled)
self.label.setStyleSheet(f"""
QLabel {{
font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
font-size: 13px;
color: {"#ffffff" if enabled else "#888888"};
}}
""")

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
智能文字編輯器
==============
支援智能 Ctrl+V 的文字輸入框,能自動處理圖片貼上。
"""
from PySide6.QtWidgets import QTextEdit, QApplication
from PySide6.QtCore import Qt, Signal
class SmartTextEdit(QTextEdit):
"""支援智能 Ctrl+V 的文字輸入框"""
image_paste_requested = Signal()
def __init__(self, parent=None):
super().__init__(parent)
def keyPressEvent(self, event):
"""處理按鍵事件,實現智能 Ctrl+V"""
if event.key() == Qt.Key_V and event.modifiers() == Qt.ControlModifier:
# 檢查剪貼簿是否有圖片
clipboard = QApplication.clipboard()
if clipboard.mimeData().hasImage():
# 如果有圖片,發送信號通知主窗口處理圖片貼上
self.image_paste_requested.emit()
# 不執行預設的文字貼上行為
return
else:
# 如果沒有圖片,執行正常的文字貼上
super().keyPressEvent(event)
else:
# 其他按鍵正常處理
super().keyPressEvent(event)

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
超時控制組件
============
提供超時設置和倒數計時器顯示功能。
"""
from PySide6.QtWidgets import (
QWidget, QHBoxLayout, QVBoxLayout, QLabel,
QSpinBox, QPushButton, QFrame
)
from PySide6.QtCore import Signal, QTimer, Qt
from PySide6.QtGui import QFont
from .switch import SwitchWidget
from ...i18n import t
from ...debug import gui_debug_log as debug_log
class TimeoutWidget(QWidget):
"""超時控制組件"""
# 信號
timeout_occurred = Signal() # 超時發生
settings_changed = Signal(bool, int) # 設置變更 (enabled, timeout_seconds)
def __init__(self, parent=None):
super().__init__(parent)
self.timeout_enabled = False
self.timeout_seconds = 600 # 預設 10 分鐘
self.remaining_seconds = 0
# 計時器
self.countdown_timer = QTimer()
self.countdown_timer.timeout.connect(self._update_countdown)
self._setup_ui()
self._connect_signals()
debug_log("超時控制組件初始化完成")
def _setup_ui(self):
"""設置用戶介面"""
# 主布局
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(8, 4, 8, 4)
main_layout.setSpacing(12)
# 超時開關區域
switch_layout = QHBoxLayout()
switch_layout.setSpacing(8)
self.timeout_label = QLabel(t('timeout.enable'))
self.timeout_label.setStyleSheet("color: #cccccc; font-size: 12px;")
switch_layout.addWidget(self.timeout_label)
self.timeout_switch = SwitchWidget()
self.timeout_switch.setToolTip(t('timeout.enableTooltip'))
switch_layout.addWidget(self.timeout_switch)
main_layout.addLayout(switch_layout)
# 分隔線
separator = QFrame()
separator.setFrameShape(QFrame.VLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("color: #464647;")
main_layout.addWidget(separator)
# 超時時間設置區域
time_layout = QHBoxLayout()
time_layout.setSpacing(8)
self.time_label = QLabel(t('timeout.duration.label'))
self.time_label.setStyleSheet("color: #cccccc; font-size: 12px;")
time_layout.addWidget(self.time_label)
self.time_spinbox = QSpinBox()
self.time_spinbox.setRange(30, 7200) # 30秒到2小時
self.time_spinbox.setValue(600) # 預設10分鐘
self.time_spinbox.setSuffix(" " + t('timeout.seconds'))
# 應用自定義樣式
style = self._get_spinbox_style(False)
self.time_spinbox.setStyleSheet(style)
debug_log("QSpinBox 樣式已應用")
time_layout.addWidget(self.time_spinbox)
main_layout.addLayout(time_layout)
# 分隔線
separator2 = QFrame()
separator2.setFrameShape(QFrame.VLine)
separator2.setFrameShadow(QFrame.Sunken)
separator2.setStyleSheet("color: #464647;")
main_layout.addWidget(separator2)
# 倒數計時器顯示區域
countdown_layout = QHBoxLayout()
countdown_layout.setSpacing(8)
self.countdown_label = QLabel(t('timeout.remaining'))
self.countdown_label.setStyleSheet("color: #cccccc; font-size: 12px;")
countdown_layout.addWidget(self.countdown_label)
self.countdown_display = QLabel("--:--")
self.countdown_display.setStyleSheet("""
color: #ffa500;
font-size: 14px;
font-weight: bold;
font-family: 'Consolas', 'Monaco', monospace;
min-width: 50px;
""")
countdown_layout.addWidget(self.countdown_display)
main_layout.addLayout(countdown_layout)
# 彈性空間
main_layout.addStretch()
# 初始狀態:隱藏倒數計時器
self._update_visibility()
def _connect_signals(self):
"""連接信號"""
self.timeout_switch.toggled.connect(self._on_timeout_enabled_changed)
self.time_spinbox.valueChanged.connect(self._on_timeout_duration_changed)
def _on_timeout_enabled_changed(self, enabled: bool):
"""超時啟用狀態變更"""
self.timeout_enabled = enabled
self._update_visibility()
if enabled:
self.start_countdown()
else:
self.stop_countdown()
self.settings_changed.emit(enabled, self.timeout_seconds)
debug_log(f"超時功能已{'啟用' if enabled else '停用'}")
def _on_timeout_duration_changed(self, seconds: int):
"""超時時間變更"""
self.timeout_seconds = seconds
# 如果正在倒數,重新開始
if self.timeout_enabled and self.countdown_timer.isActive():
self.start_countdown()
self.settings_changed.emit(self.timeout_enabled, seconds)
debug_log(f"超時時間設置為 {seconds}")
def _update_visibility(self):
"""更新組件可見性"""
# 倒數計時器只在啟用超時時顯示
self.countdown_label.setVisible(self.timeout_enabled)
self.countdown_display.setVisible(self.timeout_enabled)
# 時間設置在啟用時更明顯
style = self._get_spinbox_style(self.timeout_enabled)
self.time_spinbox.setStyleSheet(style)
debug_log(f"QSpinBox 樣式已更新 (啟用: {self.timeout_enabled})")
def start_countdown(self):
"""開始倒數計時"""
if not self.timeout_enabled:
return
self.remaining_seconds = self.timeout_seconds
self.countdown_timer.start(1000) # 每秒更新
self._update_countdown_display()
debug_log(f"開始倒數計時:{self.timeout_seconds}")
def stop_countdown(self):
"""停止倒數計時"""
self.countdown_timer.stop()
self.countdown_display.setText("--:--")
debug_log("倒數計時已停止")
def _update_countdown(self):
"""更新倒數計時"""
self.remaining_seconds -= 1
self._update_countdown_display()
if self.remaining_seconds <= 0:
self.countdown_timer.stop()
self.timeout_occurred.emit()
debug_log("倒數計時結束,觸發超時事件")
def _update_countdown_display(self):
"""更新倒數顯示"""
if self.remaining_seconds <= 0:
self.countdown_display.setText("00:00")
self.countdown_display.setStyleSheet("""
color: #ff4444;
font-size: 14px;
font-weight: bold;
font-family: 'Consolas', 'Monaco', monospace;
min-width: 50px;
""")
else:
minutes = self.remaining_seconds // 60
seconds = self.remaining_seconds % 60
time_text = f"{minutes:02d}:{seconds:02d}"
self.countdown_display.setText(time_text)
# 根據剩餘時間調整顏色
if self.remaining_seconds <= 60: # 最後1分鐘
color = "#ff4444" # 紅色
elif self.remaining_seconds <= 300: # 最後5分鐘
color = "#ffaa00" # 橙色
else:
color = "#ffa500" # 黃色
self.countdown_display.setStyleSheet(f"""
color: {color};
font-size: 14px;
font-weight: bold;
font-family: 'Consolas', 'Monaco', monospace;
min-width: 50px;
""")
def set_timeout_settings(self, enabled: bool, seconds: int):
"""設置超時參數"""
self.timeout_switch.setChecked(enabled)
self.time_spinbox.setValue(seconds)
self.timeout_enabled = enabled
self.timeout_seconds = seconds
self._update_visibility()
def get_timeout_settings(self) -> tuple[bool, int]:
"""獲取超時設置"""
return self.timeout_enabled, self.timeout_seconds
def update_texts(self):
"""更新界面文字(用於語言切換)"""
self.timeout_label.setText(t('timeout.enable'))
self.time_label.setText(t('timeout.duration.label'))
self.countdown_label.setText(t('timeout.remaining'))
self.timeout_switch.setToolTip(t('timeout.enableTooltip'))
self.time_spinbox.setSuffix(" " + t('timeout.seconds'))
def _get_spinbox_style(self, enabled: bool) -> str:
"""獲取 QSpinBox 的樣式字符串"""
border_color = "#007acc" if enabled else "#555555"
focus_color = "#0099ff" if enabled else "#007acc"
return f"""
QSpinBox {{
background-color: #3c3c3c;
border: 1px solid {border_color};
border-radius: 6px;
padding: 4px 8px;
color: #ffffff;
font-size: 12px;
min-width: 90px;
min-height: 24px;
}}
QSpinBox:focus {{
border-color: {focus_color};
background-color: #404040;
}}
QSpinBox:hover {{
background-color: #404040;
border-color: #666666;
}}
QSpinBox::up-button {{
subcontrol-origin: border;
subcontrol-position: top right;
width: 18px;
border-left: 1px solid #555555;
border-bottom: 1px solid #555555;
border-top-right-radius: 5px;
background-color: #4a4a4a;
}}
QSpinBox::up-button:hover {{
background-color: #5a5a5a;
}}
QSpinBox::up-button:pressed {{
background-color: #007acc;
}}
QSpinBox::down-button {{
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 18px;
border-left: 1px solid #555555;
border-top: 1px solid #555555;
border-bottom-right-radius: 5px;
background-color: #4a4a4a;
}}
QSpinBox::down-button:hover {{
background-color: #5a5a5a;
}}
QSpinBox::down-button:pressed {{
background-color: #007acc;
}}
QSpinBox::up-arrow {{
width: 0px;
height: 0px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 6px solid #cccccc;
}}
QSpinBox::down-arrow {{
width: 0px;
height: 0px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #cccccc;
}}
"""

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI 窗口模組
============
包含各種窗口類別。
"""
from .feedback_window import FeedbackWindow
from .config_manager import ConfigManager
from .command_executor import CommandExecutor
from .tab_manager import TabManager
__all__ = [
'FeedbackWindow',
'ConfigManager',
'CommandExecutor',
'TabManager'
]

View File

@@ -0,0 +1,242 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
命令執行管理器
===============
負責處理命令執行、輸出讀取和進程管理。
"""
import os
import subprocess
import threading
import time
import queue
import select
import sys
from typing import Optional, Callable
from PySide6.QtCore import QObject, QTimer, Signal
from ...debug import gui_debug_log as debug_log
class CommandExecutor(QObject):
"""命令執行管理器"""
output_received = Signal(str) # 輸出接收信號
def __init__(self, project_dir: str, parent=None):
super().__init__(parent)
self.project_dir = project_dir
self.command_process: Optional[subprocess.Popen] = None
self.timer: Optional[QTimer] = None
self._output_queue: Optional[queue.Queue] = None
self._reader_thread: Optional[threading.Thread] = None
self._command_start_time: Optional[float] = None
def run_command(self, command: str) -> None:
"""執行命令"""
if not command.strip():
return
# 如果已經有命令在執行,先停止
if self.timer and self.timer.isActive():
self.terminate_command()
self.output_received.emit(f"$ {command}\n")
# 保存當前命令用於輸出過濾
self._last_command = command
try:
# 準備環境變數以避免不必要的輸出
env = os.environ.copy()
env['NO_UPDATE_NOTIFIER'] = '1'
env['NPM_CONFIG_UPDATE_NOTIFIER'] = 'false'
env['NPM_CONFIG_FUND'] = 'false'
env['NPM_CONFIG_AUDIT'] = 'false'
env['PYTHONUNBUFFERED'] = '1'
# 啟動進程
self.command_process = subprocess.Popen(
command,
shell=True,
cwd=self.project_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=0,
env=env,
universal_newlines=True
)
# 設置計時器來定期讀取輸出
if not self.timer:
self.timer = QTimer()
self.timer.timeout.connect(self._read_command_output)
self.timer.start(100) # 每100ms檢查一次
self._command_start_time = time.time()
debug_log(f"命令已啟動: {command}")
except Exception as e:
self.output_received.emit(f"錯誤: 無法執行命令 - {str(e)}\n")
debug_log(f"命令執行錯誤: {e}")
def terminate_command(self) -> None:
"""終止正在運行的命令"""
if self.command_process and self.command_process.poll() is None:
try:
self.command_process.terminate()
self.output_received.emit("命令已被用戶終止。\n")
debug_log("用戶終止了正在運行的命令")
# 停止計時器
if self.timer:
self.timer.stop()
except Exception as e:
debug_log(f"終止命令失敗: {e}")
self.output_received.emit(f"終止命令失敗: {e}\n")
else:
self.output_received.emit("沒有正在運行的命令可以終止。\n")
def _read_command_output(self) -> None:
"""讀取命令輸出(非阻塞方式)"""
if not self.command_process:
if self.timer:
self.timer.stop()
return
# 檢查進程是否還在運行
if self.command_process.poll() is None:
try:
if sys.platform == "win32":
# Windows 下使用隊列方式
try:
if not self._output_queue:
self._output_queue = queue.Queue()
self._reader_thread = threading.Thread(
target=self._read_process_output_thread,
daemon=True
)
self._reader_thread.start()
# 從隊列中獲取輸出(非阻塞)
try:
while True:
output = self._output_queue.get_nowait()
if output is None: # 進程結束信號
break
self.output_received.emit(output)
except queue.Empty:
pass # 沒有新輸出,繼續等待
except ImportError:
output = self.command_process.stdout.readline()
if output:
filtered_output = self._filter_command_output(output)
if filtered_output:
self.output_received.emit(filtered_output)
else:
# Unix/Linux/macOS 下使用 select
ready, _, _ = select.select([self.command_process.stdout], [], [], 0.1)
if ready:
output = self.command_process.stdout.readline()
if output:
filtered_output = self._filter_command_output(output)
if filtered_output:
self.output_received.emit(filtered_output)
# 檢查命令執行超時30秒
if self._command_start_time and time.time() - self._command_start_time > 30:
self.output_received.emit(f"\n⚠️ 命令執行超過30秒自動終止...")
self.terminate_command()
except Exception as e:
debug_log(f"讀取命令輸出錯誤: {e}")
else:
# 進程結束,停止計時器並讀取剩餘輸出
if self.timer:
self.timer.stop()
# 清理資源
self._cleanup_resources()
try:
# 讀取剩餘的輸出
remaining_output, _ = self.command_process.communicate(timeout=2)
if remaining_output and remaining_output.strip():
filtered_output = self._filter_command_output(remaining_output)
if filtered_output:
self.output_received.emit(filtered_output)
except subprocess.TimeoutExpired:
debug_log("讀取剩餘輸出超時")
except Exception as e:
debug_log(f"讀取剩餘輸出錯誤: {e}")
return_code = self.command_process.returncode
self.output_received.emit(f"\n進程結束,返回碼: {return_code}\n")
def _read_process_output_thread(self) -> None:
"""在背景線程中讀取進程輸出"""
try:
while self.command_process and self.command_process.poll() is None:
output = self.command_process.stdout.readline()
if output:
self._output_queue.put(output)
else:
break
# 進程結束信號
if self._output_queue:
self._output_queue.put(None)
except Exception as e:
debug_log(f"背景讀取線程錯誤: {e}")
def _filter_command_output(self, output: str) -> str:
"""過濾命令輸出,移除不必要的行"""
if not output:
return ""
# 要過濾的字串(避免干擾的輸出)
filter_patterns = [
"npm notice",
"npm WARN deprecated",
"npm fund",
"npm audit",
"found 0 vulnerabilities",
"Run `npm audit` for details",
"[##", # 進度條
"⸩ ░░░░░░░░░░░░░░░░" # 其他進度指示器
]
# 檢查是否需要過濾
for pattern in filter_patterns:
if pattern in output:
return ""
return output
def _cleanup_resources(self) -> None:
"""清理資源"""
if hasattr(self, '_output_queue') and self._output_queue:
self._output_queue = None
if hasattr(self, '_reader_thread') and self._reader_thread:
self._reader_thread = None
if hasattr(self, '_command_start_time') and self._command_start_time:
self._command_start_time = None
def cleanup(self) -> None:
"""清理所有資源"""
if self.command_process and self.command_process.poll() is None:
try:
self.command_process.terminate()
debug_log("已終止正在運行的命令")
except Exception as e:
debug_log(f"終止命令失敗: {e}")
if self.timer:
self.timer.stop()
self._cleanup_resources()

View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
配置管理器
===========
負責處理用戶配置的載入、保存和管理。
"""
import json
from pathlib import Path
from typing import Dict, Any
from ...debug import gui_debug_log as debug_log
class ConfigManager:
"""配置管理器"""
def __init__(self):
self._config_file = self._get_config_file_path()
self._config_cache = {}
self._load_config()
def _get_config_file_path(self) -> Path:
"""獲取配置文件路徑"""
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir / "ui_settings.json"
def _load_config(self) -> None:
"""載入配置"""
try:
if self._config_file.exists():
with open(self._config_file, 'r', encoding='utf-8') as f:
self._config_cache = json.load(f)
debug_log("配置文件載入成功")
else:
self._config_cache = {}
debug_log("配置文件不存在,使用預設配置")
except Exception as e:
debug_log(f"載入配置失敗: {e}")
self._config_cache = {}
def _save_config(self) -> None:
"""保存配置"""
try:
with open(self._config_file, 'w', encoding='utf-8') as f:
json.dump(self._config_cache, f, ensure_ascii=False, indent=2)
debug_log("配置文件保存成功")
except Exception as e:
debug_log(f"保存配置失敗: {e}")
def get(self, key: str, default: Any = None) -> Any:
"""獲取配置值"""
return self._config_cache.get(key, default)
def set(self, key: str, value: Any) -> None:
"""設置配置值"""
self._config_cache[key] = value
self._save_config()
def update_partial_config(self, updates: Dict[str, Any]) -> None:
"""批量更新配置項目,只保存指定的設定而不影響其他參數"""
try:
# 重新載入當前磁碟上的配置,確保我們有最新的數據
current_config = {}
if self._config_file.exists():
with open(self._config_file, 'r', encoding='utf-8') as f:
current_config = json.load(f)
# 只更新指定的項目
for key, value in updates.items():
current_config[key] = value
# 同時更新內存緩存
self._config_cache[key] = value
# 保存到文件
with open(self._config_file, 'w', encoding='utf-8') as f:
json.dump(current_config, f, ensure_ascii=False, indent=2)
debug_log(f"部分配置已更新: {list(updates.keys())}")
except Exception as e:
debug_log(f"更新部分配置失敗: {e}")
def get_layout_mode(self) -> bool:
"""獲取佈局模式False=分離模式True=合併模式)"""
return self.get('combined_mode', False)
def set_layout_mode(self, combined_mode: bool) -> None:
"""設置佈局模式"""
self.update_partial_config({'combined_mode': combined_mode})
debug_log(f"佈局模式設置: {'合併模式' if combined_mode else '分離模式'}")
def get_layout_orientation(self) -> str:
"""獲取佈局方向vertical=垂直上下horizontal=水平(左右))"""
return self.get('layout_orientation', 'vertical')
def set_layout_orientation(self, orientation: str) -> None:
"""設置佈局方向"""
if orientation not in ['vertical', 'horizontal']:
orientation = 'vertical'
self.update_partial_config({'layout_orientation': orientation})
debug_log(f"佈局方向設置: {'垂直(上下)' if orientation == 'vertical' else '水平(左右)'}")
def get_language(self) -> str:
"""獲取語言設置"""
return self.get('language', 'zh-TW')
def set_language(self, language: str) -> None:
"""設置語言"""
self.update_partial_config({'language': language})
debug_log(f"語言設置: {language}")
def get_splitter_sizes(self, splitter_name: str) -> list:
"""獲取分割器尺寸"""
sizes = self.get(f'splitter_sizes.{splitter_name}', [])
if sizes:
debug_log(f"載入分割器 {splitter_name} 尺寸: {sizes}")
return sizes
def set_splitter_sizes(self, splitter_name: str, sizes: list) -> None:
"""設置分割器尺寸"""
self.update_partial_config({f'splitter_sizes.{splitter_name}': sizes})
debug_log(f"保存分割器 {splitter_name} 尺寸: {sizes}")
def get_window_geometry(self) -> dict:
"""獲取窗口幾何信息"""
geometry = self.get('window_geometry', {})
if geometry:
debug_log(f"載入窗口幾何信息: {geometry}")
return geometry
def set_window_geometry(self, geometry: dict) -> None:
"""設置窗口幾何信息(使用部分更新避免覆蓋其他設定)"""
self.update_partial_config({'window_geometry': geometry})
debug_log(f"保存窗口幾何信息: {geometry}")
def get_always_center_window(self) -> bool:
"""獲取總是在主螢幕中心顯示視窗的設置"""
return self.get('always_center_window', False)
def set_always_center_window(self, always_center: bool) -> None:
"""設置總是在主螢幕中心顯示視窗"""
self.update_partial_config({'always_center_window': always_center})
debug_log(f"視窗定位設置: {'總是中心顯示' if always_center else '智能定位'}")
def get_image_size_limit(self) -> int:
"""獲取圖片大小限制bytes0 表示無限制"""
return self.get('image_size_limit', 0)
def set_image_size_limit(self, size_bytes: int) -> None:
"""設置圖片大小限制bytes0 表示無限制"""
# 處理 None 值
if size_bytes is None:
size_bytes = 0
self.update_partial_config({'image_size_limit': size_bytes})
size_mb = size_bytes / (1024 * 1024) if size_bytes > 0 else 0
debug_log(f"圖片大小限制設置: {'無限制' if size_bytes == 0 else f'{size_mb:.1f}MB'}")
def get_enable_base64_detail(self) -> bool:
"""獲取是否啟用 Base64 詳細模式"""
return self.get('enable_base64_detail', False)
def set_enable_base64_detail(self, enabled: bool) -> None:
"""設置是否啟用 Base64 詳細模式"""
self.update_partial_config({'enable_base64_detail': enabled})
debug_log(f"Base64 詳細模式設置: {'啟用' if enabled else '停用'}")
def get_timeout_enabled(self) -> bool:
"""獲取是否啟用超時自動關閉"""
return self.get('timeout_enabled', False)
def set_timeout_enabled(self, enabled: bool) -> None:
"""設置是否啟用超時自動關閉"""
self.update_partial_config({'timeout_enabled': enabled})
debug_log(f"超時自動關閉設置: {'啟用' if enabled else '停用'}")
def get_timeout_duration(self) -> int:
"""獲取超時時間(秒)"""
return self.get('timeout_duration', 600) # 預設10分鐘
def set_timeout_duration(self, seconds: int) -> None:
"""設置超時時間(秒)"""
self.update_partial_config({'timeout_duration': seconds})
debug_log(f"超時時間設置: {seconds}")
def get_timeout_settings(self) -> tuple[bool, int]:
"""獲取超時設置(啟用狀態, 超時時間)"""
return self.get_timeout_enabled(), self.get_timeout_duration()
def set_timeout_settings(self, enabled: bool, seconds: int) -> None:
"""設置超時設置"""
self.update_partial_config({
'timeout_enabled': enabled,
'timeout_duration': seconds
})
debug_log(f"超時設置: {'啟用' if enabled else '停用'}, {seconds}")
def reset_settings(self) -> None:
"""重置所有設定到預設值"""
try:
# 清空配置緩存
self._config_cache = {}
# 刪除配置文件
if self._config_file.exists():
self._config_file.unlink()
debug_log("配置文件已刪除")
debug_log("所有設定已重置到預設值")
except Exception as e:
debug_log(f"重置設定失敗: {e}")
raise

View File

@@ -0,0 +1,791 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回饋收集主窗口(重構版)
========================
簡化的主窗口,專注於主要職責:窗口管理和協調各組件。
"""
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QTabWidget, QPushButton, QMessageBox, QScrollArea, QSizePolicy
)
from PySide6.QtCore import Signal, Qt, QTimer
from PySide6.QtGui import QKeySequence, QShortcut
from .config_manager import ConfigManager
from .tab_manager import TabManager
from ..utils import apply_widget_styles
from ...i18n import t, get_i18n_manager
from ...debug import gui_debug_log as debug_log
class FeedbackWindow(QMainWindow):
"""回饋收集主窗口(重構版)"""
language_changed = Signal()
timeout_occurred = Signal() # 超時發生信號
def __init__(self, project_dir: str, summary: str, timeout_seconds: int = None):
super().__init__()
self.project_dir = project_dir
self.summary = summary
self.result = None
self.i18n = get_i18n_manager()
self.mcp_timeout_seconds = timeout_seconds # MCP 傳入的超時時間
# 初始化組件
self.config_manager = ConfigManager()
# 載入保存的語言設定
saved_language = self.config_manager.get_language()
if saved_language:
self.i18n.set_language(saved_language)
self.combined_mode = self.config_manager.get_layout_mode()
self.layout_orientation = self.config_manager.get_layout_orientation()
# 設置窗口狀態保存的防抖計時器
self._save_timer = QTimer()
self._save_timer.setSingleShot(True)
self._save_timer.timeout.connect(self._delayed_save_window_position)
self._save_delay = 500 # 500ms 延遲,避免過於頻繁的保存
# 設置UI
self._setup_ui()
self._setup_shortcuts()
self._connect_signals()
debug_log("主窗口初始化完成")
# 如果啟用了超時,自動開始倒數計時
self.start_timeout_if_enabled()
def _setup_ui(self) -> None:
"""設置用戶介面"""
self.setWindowTitle(t('app.title'))
self.setMinimumSize(400, 300) # 大幅降低最小窗口大小限制,允許用戶自由調整
self.resize(1200, 900)
# 智能視窗定位
self._apply_window_positioning()
# 中央元件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(8)
main_layout.setContentsMargins(16, 8, 16, 12)
# 頂部專案目錄信息
self._create_project_header(main_layout)
# 分頁區域
self._create_tab_area(main_layout)
# 操作按鈕
self._create_action_buttons(main_layout)
# 應用深色主題
self._apply_dark_style()
def _create_project_header(self, layout: QVBoxLayout) -> None:
"""創建專案目錄頭部信息"""
# 創建水平布局來放置專案目錄和倒數計時器
header_layout = QHBoxLayout()
self.project_label = QLabel(f"{t('app.projectDirectory')}: {self.project_dir}")
self.project_label.setStyleSheet("color: #9e9e9e; font-size: 12px; padding: 4px 0;")
header_layout.addWidget(self.project_label)
# 添加彈性空間
header_layout.addStretch()
# 添加倒數計時器顯示(僅顯示部分)
self._create_countdown_display(header_layout)
# 將水平布局添加到主布局
header_widget = QWidget()
header_widget.setLayout(header_layout)
layout.addWidget(header_widget)
def _create_countdown_display(self, layout: QHBoxLayout) -> None:
"""創建倒數計時器顯示組件(僅顯示)"""
# 倒數計時器標籤
self.countdown_label = QLabel(t('timeout.remaining'))
self.countdown_label.setStyleSheet("color: #cccccc; font-size: 12px;")
self.countdown_label.setVisible(False) # 預設隱藏
layout.addWidget(self.countdown_label)
# 倒數計時器顯示
self.countdown_display = QLabel("--:--")
self.countdown_display.setStyleSheet("""
color: #ffa500;
font-size: 14px;
font-weight: bold;
font-family: 'Consolas', 'Monaco', monospace;
min-width: 50px;
margin-left: 8px;
""")
self.countdown_display.setVisible(False) # 預設隱藏
layout.addWidget(self.countdown_display)
# 初始化超時控制邏輯
self._init_timeout_logic()
def _init_timeout_logic(self) -> None:
"""初始化超時控制邏輯"""
# 載入保存的超時設置
timeout_enabled, timeout_duration = self.config_manager.get_timeout_settings()
# 如果有 MCP 超時參數,且用戶設置的時間大於 MCP 時間,則使用 MCP 時間
if self.mcp_timeout_seconds is not None:
if timeout_duration > self.mcp_timeout_seconds:
timeout_duration = self.mcp_timeout_seconds
debug_log(f"用戶設置的超時時間 ({timeout_duration}s) 大於 MCP 超時時間 ({self.mcp_timeout_seconds}s),使用 MCP 時間")
# 保存超時設置
self.timeout_enabled = timeout_enabled
self.timeout_duration = timeout_duration
self.remaining_seconds = 0
# 創建計時器
self.countdown_timer = QTimer()
self.countdown_timer.timeout.connect(self._update_countdown)
# 更新顯示狀態
self._update_countdown_visibility()
def _create_tab_area(self, layout: QVBoxLayout) -> None:
"""創建分頁區域"""
# 創建滾動區域來包裝整個分頁組件
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll_area.setMinimumHeight(150) # 降低滾動區域最小高度,支持小窗口
scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #464647;
border-radius: 4px;
background-color: #2b2b2b;
}
QScrollArea > QWidget > QWidget {
background-color: #2b2b2b;
}
QScrollArea QScrollBar:vertical {
background-color: #2a2a2a;
width: 8px;
border-radius: 4px;
margin: 0;
}
QScrollArea QScrollBar::handle:vertical {
background-color: #555;
border-radius: 4px;
min-height: 20px;
margin: 1px;
}
QScrollArea QScrollBar::handle:vertical:hover {
background-color: #777;
}
QScrollArea QScrollBar::add-line:vertical,
QScrollArea QScrollBar::sub-line:vertical {
border: none;
background: none;
height: 0px;
}
QScrollArea QScrollBar:horizontal {
background-color: #2a2a2a;
height: 8px;
border-radius: 4px;
margin: 0;
}
QScrollArea QScrollBar::handle:horizontal {
background-color: #555;
border-radius: 4px;
min-width: 20px;
margin: 1px;
}
QScrollArea QScrollBar::handle:horizontal:hover {
background-color: #777;
}
QScrollArea QScrollBar::add-line:horizontal,
QScrollArea QScrollBar::sub-line:horizontal {
border: none;
background: none;
width: 0px;
}
""")
self.tab_widget = QTabWidget()
self.tab_widget.setMinimumHeight(150) # 降低分頁組件最小高度
# 設置分頁組件的大小策略,確保能觸發滾動
self.tab_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 初始化分頁管理器
self.tab_manager = TabManager(
self.tab_widget,
self.project_dir,
self.summary,
self.combined_mode,
self.layout_orientation
)
# 創建分頁
self.tab_manager.create_tabs()
# 連接分頁信號
self.tab_manager.connect_signals(self)
# 將分頁組件放入滾動區域
scroll_area.setWidget(self.tab_widget)
layout.addWidget(scroll_area, 1)
def _create_action_buttons(self, layout: QVBoxLayout) -> None:
"""創建操作按鈕"""
button_layout = QHBoxLayout()
button_layout.addStretch()
# 取消按鈕
self.cancel_button = QPushButton(t('buttons.cancel'))
self.cancel_button.clicked.connect(self._cancel_feedback)
self.cancel_button.setFixedSize(130, 40)
apply_widget_styles(self.cancel_button, "secondary_button")
button_layout.addWidget(self.cancel_button)
# 提交按鈕
self.submit_button = QPushButton(t('buttons.submit'))
self.submit_button.clicked.connect(self._submit_feedback)
self.submit_button.setFixedSize(160, 40)
self.submit_button.setDefault(True)
apply_widget_styles(self.submit_button, "success_button")
button_layout.addWidget(self.submit_button)
layout.addLayout(button_layout)
def _setup_shortcuts(self) -> None:
"""設置快捷鍵"""
# Ctrl+Enter (主鍵盤) 提交回饋
submit_shortcut_main = QShortcut(QKeySequence("Ctrl+Return"), self)
submit_shortcut_main.activated.connect(self._submit_feedback)
# Ctrl+Enter (數字鍵盤) 提交回饋
submit_shortcut_keypad = QShortcut(QKeySequence(Qt.Modifier.CTRL | Qt.Key.Key_Enter), self)
submit_shortcut_keypad.activated.connect(self._submit_feedback)
# macOS 支援 Cmd+Return (主鍵盤)
submit_shortcut_mac_main = QShortcut(QKeySequence("Meta+Return"), self)
submit_shortcut_mac_main.activated.connect(self._submit_feedback)
# macOS 支援 Cmd+Enter (數字鍵盤)
submit_shortcut_mac_keypad = QShortcut(QKeySequence(Qt.Modifier.META | Qt.Key.Key_Enter), self)
submit_shortcut_mac_keypad.activated.connect(self._submit_feedback)
# Escape 取消回饋
cancel_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self)
cancel_shortcut.activated.connect(self._cancel_feedback)
def _connect_signals(self) -> None:
"""連接信號"""
# 連接語言變更信號
self.language_changed.connect(self._refresh_ui_texts)
# 連接分頁管理器的信號
self.tab_manager.connect_signals(self)
def _apply_dark_style(self) -> None:
"""應用深色主題"""
self.setStyleSheet("""
QMainWindow {
background-color: #2b2b2b;
color: #ffffff;
}
QGroupBox {
font-weight: bold;
border: 2px solid #464647;
border-radius: 8px;
margin-top: 1ex;
padding: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QTextEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 8px;
color: #ffffff;
}
QLineEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 8px;
color: #ffffff;
}
QTabWidget::pane {
border: 1px solid #464647;
border-radius: 4px;
background-color: #2b2b2b;
}
QTabBar::tab {
background-color: #2d2d30;
color: #ffffff;
border: 1px solid #464647;
padding: 8px 16px;
margin-right: 2px;
}
QTabBar::tab:selected {
background-color: #007acc;
}
QSplitter {
background-color: #2b2b2b;
}
QSplitter::handle {
background-color: #3c3c3c;
border: 1px solid #555555;
border-radius: 3px;
margin: 0px;
}
QSplitter::handle:horizontal {
width: 6px;
background-color: #3c3c3c;
border: 1px solid #555555;
border-radius: 3px;
margin: 0px;
}
QSplitter::handle:vertical {
height: 6px;
background-color: #3c3c3c;
border: 1px solid #555555;
border-radius: 3px;
margin: 0px;
}
QSplitter::handle:hover {
background-color: #606060;
border-color: #808080;
}
QSplitter::handle:pressed {
background-color: #007acc;
border-color: #005a9e;
}
""")
def _on_layout_change_requested(self, combined_mode: bool, orientation: str) -> None:
"""處理佈局變更請求(模式和方向同時變更)"""
try:
# 保存當前內容
current_data = self.tab_manager.get_feedback_data()
# 記住當前分頁索引
current_tab_index = self.tab_widget.currentIndex()
# 保存新設置
self.combined_mode = combined_mode
self.layout_orientation = orientation
self.config_manager.set_layout_mode(combined_mode)
self.config_manager.set_layout_orientation(orientation)
# 重新創建分頁
self.tab_manager.set_layout_mode(combined_mode)
self.tab_manager.set_layout_orientation(orientation)
self.tab_manager.create_tabs()
# 恢復內容
self.tab_manager.restore_content(
current_data["interactive_feedback"],
current_data["command_logs"],
current_data["images"]
)
# 重新連接信號
self.tab_manager.connect_signals(self)
# 刷新UI文字
self._refresh_ui_texts()
# 恢復到設定頁面(通常是倒數第二個分頁)
if self.combined_mode:
# 合併模式:回饋、命令、設置、關於
settings_tab_index = 2
else:
# 分離模式:回饋、摘要、命令、設置、關於
settings_tab_index = 3
# 確保索引在有效範圍內
if settings_tab_index < self.tab_widget.count():
self.tab_widget.setCurrentIndex(settings_tab_index)
mode_text = "合併模式" if combined_mode else "分離模式"
orientation_text = "(水平布局)" if orientation == "horizontal" else "(垂直布局)"
if combined_mode:
mode_text += orientation_text
debug_log(f"佈局已切換到: {mode_text}")
except Exception as e:
debug_log(f"佈局變更失敗: {e}")
QMessageBox.warning(self, t('errors.title'), t('errors.interfaceReloadError', error=str(e)))
def _on_reset_settings_requested(self) -> None:
"""處理重置設定請求"""
try:
# 重置配置管理器的所有設定
self.config_manager.reset_settings()
# 重置應用程式狀態
self.combined_mode = False # 重置為分離模式
self.layout_orientation = 'vertical' # 重置為垂直布局
# 重新設置語言為預設
self.i18n.set_language('zh-TW')
# 保存當前內容
current_data = self.tab_manager.get_feedback_data()
# 重新創建分頁
self.tab_manager.set_layout_mode(self.combined_mode)
self.tab_manager.set_layout_orientation(self.layout_orientation)
self.tab_manager.create_tabs()
# 恢復內容
self.tab_manager.restore_content(
current_data["interactive_feedback"],
current_data["command_logs"],
current_data["images"]
)
# 重新連接信號
self.tab_manager.connect_signals(self)
# 重新載入設定分頁的狀態
if self.tab_manager.settings_tab:
self.tab_manager.settings_tab.reload_settings_from_config()
# 刷新UI文字
self._refresh_ui_texts()
# 重新應用視窗定位(使用重置後的設定)
self._apply_window_positioning()
# 切換到設定分頁顯示重置結果
settings_tab_index = 3 # 分離模式下設定分頁是第4個索引3
if settings_tab_index < self.tab_widget.count():
self.tab_widget.setCurrentIndex(settings_tab_index)
# 顯示成功訊息
QMessageBox.information(
self,
t('settings.reset.successTitle'),
t('settings.reset.successMessage'),
QMessageBox.Ok
)
debug_log("設定重置成功")
except Exception as e:
debug_log(f"重置設定失敗: {e}")
QMessageBox.critical(
self,
t('errors.title'),
t('settings.reset.error', error=str(e)),
QMessageBox.Ok
)
def _submit_feedback(self) -> None:
"""提交回饋"""
# 獲取所有回饋數據
data = self.tab_manager.get_feedback_data()
self.result = data
debug_log(f"回饋提交: 文字長度={len(data['interactive_feedback'])}, "
f"命令日誌長度={len(data['command_logs'])}, "
f"圖片數量={len(data['images'])}")
# 關閉窗口
self.close()
def _cancel_feedback(self) -> None:
"""取消回饋收集"""
debug_log("取消回饋收集")
self.result = ""
self.close()
def force_close(self) -> None:
"""強制關閉視窗(用於超時處理)"""
debug_log("強制關閉視窗(超時)")
self.result = ""
self.close()
def _on_timeout_occurred(self) -> None:
"""處理超時事件"""
debug_log("用戶設置的超時時間已到,自動關閉視窗")
self._timeout_occurred = True
self.timeout_occurred.emit()
self.force_close()
def start_timeout_if_enabled(self) -> None:
"""如果啟用了超時,自動開始倒數計時"""
if hasattr(self, 'tab_manager') and self.tab_manager:
timeout_widget = self.tab_manager.get_timeout_widget()
if timeout_widget:
enabled, _ = timeout_widget.get_timeout_settings()
if enabled:
timeout_widget.start_countdown()
debug_log("窗口顯示時自動開始倒數計時")
def _on_timeout_settings_changed(self, enabled: bool, seconds: int) -> None:
"""處理超時設置變更(從設置頁籤觸發)"""
# 檢查是否超過 MCP 超時限制
if self.mcp_timeout_seconds is not None and seconds > self.mcp_timeout_seconds:
debug_log(f"用戶設置的超時時間 ({seconds}s) 超過 MCP 限制 ({self.mcp_timeout_seconds}s),調整為 MCP 時間")
seconds = self.mcp_timeout_seconds
# 更新內部狀態
self.timeout_enabled = enabled
self.timeout_duration = seconds
# 保存設置
self.config_manager.set_timeout_settings(enabled, seconds)
debug_log(f"超時設置已更新: {'啟用' if enabled else '停用'}, {seconds}")
# 更新倒數計時器顯示
self._update_countdown_visibility()
# 重新開始倒數計時
if enabled:
self.start_countdown()
else:
self.stop_countdown()
def start_timeout_if_enabled(self) -> None:
"""如果啟用了超時,開始倒數計時"""
if self.timeout_enabled:
self.start_countdown()
debug_log("超時倒數計時已開始")
def stop_timeout(self) -> None:
"""停止超時倒數計時"""
self.stop_countdown()
debug_log("超時倒數計時已停止")
def start_countdown(self) -> None:
"""開始倒數計時"""
if not self.timeout_enabled:
return
self.remaining_seconds = self.timeout_duration
self.countdown_timer.start(1000) # 每秒更新
self._update_countdown_display()
debug_log(f"開始倒數計時:{self.timeout_duration}")
def stop_countdown(self) -> None:
"""停止倒數計時"""
self.countdown_timer.stop()
self.countdown_display.setText("--:--")
debug_log("倒數計時已停止")
def _update_countdown(self) -> None:
"""更新倒數計時"""
self.remaining_seconds -= 1
self._update_countdown_display()
if self.remaining_seconds <= 0:
self.countdown_timer.stop()
self._on_timeout_occurred()
debug_log("倒數計時結束,觸發超時事件")
def _update_countdown_display(self) -> None:
"""更新倒數顯示"""
if self.remaining_seconds <= 0:
self.countdown_display.setText("00:00")
self.countdown_display.setStyleSheet("""
color: #ff4444;
font-size: 14px;
font-weight: bold;
font-family: 'Consolas', 'Monaco', monospace;
min-width: 50px;
margin-left: 8px;
""")
else:
minutes = self.remaining_seconds // 60
seconds = self.remaining_seconds % 60
time_text = f"{minutes:02d}:{seconds:02d}"
self.countdown_display.setText(time_text)
# 根據剩餘時間調整顏色
if self.remaining_seconds <= 60: # 最後1分鐘
color = "#ff4444" # 紅色
elif self.remaining_seconds <= 300: # 最後5分鐘
color = "#ffaa00" # 橙色
else:
color = "#ffa500" # 黃色
self.countdown_display.setStyleSheet(f"""
color: {color};
font-size: 14px;
font-weight: bold;
font-family: 'Consolas', 'Monaco', monospace;
min-width: 50px;
margin-left: 8px;
""")
def _update_countdown_visibility(self) -> None:
"""更新倒數計時器可見性"""
# 倒數計時器只在啟用超時時顯示
self.countdown_label.setVisible(self.timeout_enabled)
self.countdown_display.setVisible(self.timeout_enabled)
def _refresh_ui_texts(self) -> None:
"""刷新界面文字"""
self.setWindowTitle(t('app.title'))
self.project_label.setText(f"{t('app.projectDirectory')}: {self.project_dir}")
# 更新按鈕文字
self.submit_button.setText(t('buttons.submit'))
self.cancel_button.setText(t('buttons.cancel'))
# 更新倒數計時器文字
if hasattr(self, 'countdown_label'):
self.countdown_label.setText(t('timeout.remaining'))
# 更新分頁文字
self.tab_manager.update_tab_texts()
def _apply_window_positioning(self) -> None:
"""根據用戶設置應用視窗定位策略"""
always_center = self.config_manager.get_always_center_window()
if always_center:
# 總是中心顯示模式:使用保存的大小(如果有的話),然後置中
self._restore_window_size_only()
self._move_to_primary_screen_center()
else:
# 智能定位模式:先嘗試恢復上次完整的位置和大小
if self._restore_last_position():
# 檢查恢復的位置是否可見
if not self._is_window_visible():
self._move_to_primary_screen_center()
else:
# 沒有保存的位置,移到中心
self._move_to_primary_screen_center()
def _is_window_visible(self) -> bool:
"""檢查視窗是否在任何螢幕的可見範圍內"""
from PySide6.QtWidgets import QApplication
window_rect = self.frameGeometry()
for screen in QApplication.screens():
if screen.availableGeometry().intersects(window_rect):
return True
return False
def _move_to_primary_screen_center(self) -> None:
"""將視窗移到主螢幕中心"""
from PySide6.QtWidgets import QApplication
screen = QApplication.primaryScreen()
if screen:
screen_geometry = screen.availableGeometry()
window_geometry = self.frameGeometry()
center_point = screen_geometry.center()
window_geometry.moveCenter(center_point)
self.move(window_geometry.topLeft())
debug_log("視窗已移到主螢幕中心")
def _restore_window_size_only(self) -> bool:
"""只恢復視窗大小(不恢復位置)"""
try:
geometry = self.config_manager.get_window_geometry()
if geometry and 'width' in geometry and 'height' in geometry:
self.resize(geometry['width'], geometry['height'])
debug_log(f"已恢復視窗大小: {geometry['width']}x{geometry['height']}")
return True
except Exception as e:
debug_log(f"恢復視窗大小失敗: {e}")
return False
def _restore_last_position(self) -> bool:
"""嘗試恢復上次保存的視窗位置和大小"""
try:
geometry = self.config_manager.get_window_geometry()
if geometry and 'x' in geometry and 'y' in geometry and 'width' in geometry and 'height' in geometry:
self.move(geometry['x'], geometry['y'])
self.resize(geometry['width'], geometry['height'])
debug_log(f"已恢復視窗位置: ({geometry['x']}, {geometry['y']}) 大小: {geometry['width']}x{geometry['height']}")
return True
except Exception as e:
debug_log(f"恢復視窗位置失敗: {e}")
return False
def _save_window_position(self) -> None:
"""保存當前視窗位置和大小"""
try:
always_center = self.config_manager.get_always_center_window()
# 獲取當前幾何信息
current_geometry = {
'width': self.width(),
'height': self.height()
}
if not always_center:
# 智能定位模式:同時保存位置
current_geometry['x'] = self.x()
current_geometry['y'] = self.y()
debug_log(f"已保存視窗位置: ({current_geometry['x']}, {current_geometry['y']}) 大小: {current_geometry['width']}x{current_geometry['height']}")
else:
# 總是中心顯示模式:只保存大小,不保存位置
debug_log(f"已保存視窗大小: {current_geometry['width']}x{current_geometry['height']} (總是中心顯示模式)")
# 獲取現有配置,只更新需要的部分
saved_geometry = self.config_manager.get_window_geometry() or {}
saved_geometry.update(current_geometry)
self.config_manager.set_window_geometry(saved_geometry)
except Exception as e:
debug_log(f"保存視窗狀態失敗: {e}")
def resizeEvent(self, event) -> None:
"""窗口大小變化事件"""
super().resizeEvent(event)
# 窗口大小變化時始終保存(無論是否設置為中心顯示)
if hasattr(self, 'config_manager'):
self._schedule_save_window_position()
def moveEvent(self, event) -> None:
"""窗口位置變化事件"""
super().moveEvent(event)
# 窗口位置變化只在智能定位模式下保存
if hasattr(self, 'config_manager') and not self.config_manager.get_always_center_window():
self._schedule_save_window_position()
def _schedule_save_window_position(self) -> None:
"""調度窗口位置保存(防抖機制)"""
if hasattr(self, '_save_timer'):
self._save_timer.start(self._save_delay)
def _delayed_save_window_position(self) -> None:
"""延遲保存窗口位置(防抖機制的實際執行)"""
self._save_window_position()
def closeEvent(self, event) -> None:
"""窗口關閉事件"""
# 最終保存視窗狀態(大小始終保存,位置根據設置決定)
self._save_window_position()
# 清理分頁管理器
self.tab_manager.cleanup()
event.accept()
debug_log("主窗口已關閉")

View File

@@ -0,0 +1,358 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
分頁管理器
==========
負責管理和創建各種分頁組件。
"""
from typing import Dict, Any
from PySide6.QtWidgets import QTabWidget, QSplitter, QWidget, QVBoxLayout, QScrollArea, QSizePolicy
from PySide6.QtCore import Signal, Qt
from ..tabs import FeedbackTab, SummaryTab, CommandTab, SettingsTab, AboutTab
from ..widgets import SmartTextEdit, ImageUploadWidget
from ...i18n import t
from ...debug import gui_debug_log as debug_log
from .config_manager import ConfigManager
class TabManager:
"""分頁管理器"""
def __init__(self, tab_widget: QTabWidget, project_dir: str, summary: str, combined_mode: bool, layout_orientation: str = 'vertical'):
self.tab_widget = tab_widget
self.project_dir = project_dir
self.summary = summary
self.combined_mode = combined_mode
self.layout_orientation = layout_orientation
# 配置管理器
self.config_manager = ConfigManager()
# 分頁組件實例
self.feedback_tab = None
self.summary_tab = None
self.command_tab = None
self.settings_tab = None
self.about_tab = None
self.combined_feedback_tab = None
def create_tabs(self) -> None:
"""創建所有分頁"""
# 清除現有分頁
self.tab_widget.clear()
if self.combined_mode:
# 合併模式回饋頁包含AI摘要
self._create_combined_feedback_tab()
self.tab_widget.addTab(self.combined_feedback_tab, t('tabs.feedback'))
else:
# 分離模式:分別的回饋和摘要頁
self.feedback_tab = FeedbackTab()
self.tab_widget.addTab(self.feedback_tab, t('tabs.feedback'))
self.summary_tab = SummaryTab(self.summary)
self.tab_widget.addTab(self.summary_tab, t('tabs.summary'))
# 命令分頁
self.command_tab = CommandTab(self.project_dir)
self.tab_widget.addTab(self.command_tab, t('tabs.command'))
# 設置分頁
self.settings_tab = SettingsTab(self.combined_mode, self.config_manager)
self.settings_tab.set_layout_orientation(self.layout_orientation)
self.tab_widget.addTab(self.settings_tab, t('tabs.language'))
# 關於分頁
self.about_tab = AboutTab()
self.tab_widget.addTab(self.about_tab, t('tabs.about'))
debug_log(f"分頁創建完成,模式: {'合併' if self.combined_mode else '分離'},方向: {self.layout_orientation}")
def _create_combined_feedback_tab(self) -> None:
"""創建合併模式的回饋分頁包含AI摘要"""
self.combined_feedback_tab = QWidget()
# 主布局
tab_layout = QVBoxLayout(self.combined_feedback_tab)
tab_layout.setSpacing(12)
tab_layout.setContentsMargins(0, 0, 0, 0) # 設置邊距為0
# 創建分割器包裝容器
splitter_wrapper = QWidget()
splitter_wrapper_layout = QVBoxLayout(splitter_wrapper)
splitter_wrapper_layout.setContentsMargins(16, 16, 16, 0) # 恢復左右邊距設置
splitter_wrapper_layout.setSpacing(0)
# 根據布局方向創建分割器
orientation = Qt.Horizontal if self.layout_orientation == 'horizontal' else Qt.Vertical
main_splitter = QSplitter(orientation)
main_splitter.setChildrenCollapsible(False)
main_splitter.setHandleWidth(6)
main_splitter.setContentsMargins(0, 0, 0, 0) # 設置分割器邊距為0
# 設置分割器wrapper樣式確保分割器延伸到邊緣
splitter_wrapper.setStyleSheet("""
QWidget {
margin: 0px;
padding: 0px;
}
""")
# 根據方向設置不同的分割器樣式
if self.layout_orientation == 'horizontal':
# 水平布局(左右)
main_splitter.setStyleSheet("""
QSplitter {
border: none;
background: transparent;
}
QSplitter::handle:horizontal {
width: 8px;
background-color: #3c3c3c;
border: 1px solid #555555;
border-radius: 4px;
margin-top: 16px;
margin-bottom: 16px;
margin-left: 2px;
margin-right: 2px;
}
QSplitter::handle:horizontal:hover {
background-color: #606060;
border-color: #808080;
}
QSplitter::handle:horizontal:pressed {
background-color: #007acc;
border-color: #005a9e;
}
""")
else:
# 垂直布局(上下)
main_splitter.setStyleSheet("""
QSplitter {
border: none;
background: transparent;
}
QSplitter::handle:vertical {
height: 8px;
background-color: #3c3c3c;
border: 1px solid #555555;
border-radius: 4px;
margin-left: 16px;
margin-right: 16px;
margin-top: 2px;
margin-bottom: 2px;
}
QSplitter::handle:vertical:hover {
background-color: #606060;
border-color: #808080;
}
QSplitter::handle:vertical:pressed {
background-color: #007acc;
border-color: #005a9e;
}
""")
# 創建AI摘要組件
self.summary_tab = SummaryTab(self.summary)
# 創建回饋輸入組件
self.feedback_tab = FeedbackTab()
if self.layout_orientation == 'horizontal':
# 水平布局設置
self.summary_tab.setMinimumWidth(150) # 降低最小寬度
self.summary_tab.setMaximumWidth(800)
self.feedback_tab.setMinimumWidth(200) # 降低最小寬度
self.feedback_tab.setMaximumWidth(1200)
# 添加到主分割器
main_splitter.addWidget(self.summary_tab)
main_splitter.addWidget(self.feedback_tab)
# 調整分割器比例(水平布局)
main_splitter.setStretchFactor(0, 1) # AI摘要區域
main_splitter.setStretchFactor(1, 2) # 回饋輸入區域
# 從配置載入分割器位置
saved_sizes = self.config_manager.get_splitter_sizes('main_splitter_horizontal')
if saved_sizes and len(saved_sizes) == 2:
main_splitter.setSizes(saved_sizes)
else:
main_splitter.setSizes([400, 600]) # 預設大小(水平)
# 連接分割器位置變化信號
main_splitter.splitterMoved.connect(
lambda pos, index: self._save_splitter_position(main_splitter, 'main_splitter_horizontal')
)
# 設置最小高度
main_splitter.setMinimumHeight(200) # 降低水平布局最小高度
main_splitter.setMaximumHeight(2000)
else:
# 垂直布局設置
self.summary_tab.setMinimumHeight(80) # 降低摘要最小高度
self.summary_tab.setMaximumHeight(1000)
self.feedback_tab.setMinimumHeight(120) # 降低回饋最小高度
self.feedback_tab.setMaximumHeight(2000)
# 添加到主分割器
main_splitter.addWidget(self.summary_tab)
main_splitter.addWidget(self.feedback_tab)
# 調整分割器比例(垂直布局)
main_splitter.setStretchFactor(0, 1) # AI摘要區域
main_splitter.setStretchFactor(1, 2) # 回饋輸入區域
# 從配置載入分割器位置
saved_sizes = self.config_manager.get_splitter_sizes('main_splitter_vertical')
if saved_sizes and len(saved_sizes) == 2:
main_splitter.setSizes(saved_sizes)
else:
main_splitter.setSizes([160, 480]) # 預設大小(垂直)
# 連接分割器位置變化信號
main_splitter.splitterMoved.connect(
lambda pos, index: self._save_splitter_position(main_splitter, 'main_splitter_vertical')
)
# 設置最小高度
main_splitter.setMinimumHeight(200) # 降低垂直布局最小高度
main_splitter.setMaximumHeight(3000)
splitter_wrapper_layout.addWidget(main_splitter)
# 添加底部空間以保持完整的邊距
bottom_spacer = QWidget()
bottom_spacer.setFixedHeight(16)
tab_layout.addWidget(splitter_wrapper, 1)
tab_layout.addWidget(bottom_spacer)
# 設置合併分頁的大小策略,確保能夠觸發父容器的滾動條
self.combined_feedback_tab.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
if self.layout_orientation == 'vertical':
self.combined_feedback_tab.setMinimumHeight(200) # 降低垂直布局最小高度
else:
self.combined_feedback_tab.setMinimumWidth(400) # 降低水平布局最小寬度
def update_tab_texts(self) -> None:
"""更新分頁標籤文字"""
if self.combined_mode:
# 合併模式:回饋、命令、設置、關於
self.tab_widget.setTabText(0, t('tabs.feedback'))
self.tab_widget.setTabText(1, t('tabs.command'))
self.tab_widget.setTabText(2, t('tabs.language'))
self.tab_widget.setTabText(3, t('tabs.about'))
else:
# 分離模式:回饋、摘要、命令、設置、關於
self.tab_widget.setTabText(0, t('tabs.feedback'))
self.tab_widget.setTabText(1, t('tabs.summary'))
self.tab_widget.setTabText(2, t('tabs.command'))
self.tab_widget.setTabText(3, t('tabs.language'))
self.tab_widget.setTabText(4, t('tabs.about'))
# 更新各分頁的內部文字
if self.feedback_tab:
self.feedback_tab.update_texts()
if self.summary_tab:
self.summary_tab.update_texts()
if self.command_tab:
self.command_tab.update_texts()
if self.settings_tab:
self.settings_tab.update_texts()
if self.about_tab:
self.about_tab.update_texts()
def get_feedback_data(self) -> Dict[str, Any]:
"""獲取回饋數據"""
result = {
"interactive_feedback": "",
"command_logs": "",
"images": [],
"settings": {}
}
# 獲取回饋文字和圖片
if self.feedback_tab:
result["interactive_feedback"] = self.feedback_tab.get_feedback_text()
result["images"] = self.feedback_tab.get_images_data()
# 獲取命令日誌
if self.command_tab:
result["command_logs"] = self.command_tab.get_command_logs()
# 獲取圖片設定
if self.config_manager:
result["settings"] = {
"image_size_limit": self.config_manager.get_image_size_limit(),
"enable_base64_detail": self.config_manager.get_enable_base64_detail()
}
return result
def restore_content(self, feedback_text: str, command_logs: str, images_data: list) -> None:
"""恢復內容(用於界面重新創建時)"""
try:
if self.feedback_tab and feedback_text:
if hasattr(self.feedback_tab, 'feedback_input'):
self.feedback_tab.feedback_input.setPlainText(feedback_text)
if self.command_tab and command_logs:
if hasattr(self.command_tab, 'command_output'):
self.command_tab.command_output.setPlainText(command_logs)
if self.feedback_tab and images_data:
if hasattr(self.feedback_tab, 'image_upload'):
for img_data in images_data:
try:
self.feedback_tab.image_upload.add_image_data(img_data)
except:
pass # 如果無法恢復圖片,忽略錯誤
debug_log("內容恢復完成")
except Exception as e:
debug_log(f"恢復內容失敗: {e}")
def connect_signals(self, parent) -> None:
"""連接信號"""
# 連接設置分頁的信號
if self.settings_tab:
# 語言變更信號直接連接到父窗口的刷新方法
if hasattr(parent, '_refresh_ui_texts'):
self.settings_tab.language_changed.connect(parent._refresh_ui_texts)
if hasattr(parent, '_on_layout_change_requested'):
self.settings_tab.layout_change_requested.connect(parent._on_layout_change_requested)
if hasattr(parent, '_on_reset_settings_requested'):
self.settings_tab.reset_requested.connect(parent._on_reset_settings_requested)
if hasattr(parent, '_on_timeout_settings_changed'):
self.settings_tab.timeout_settings_changed.connect(parent._on_timeout_settings_changed)
# 圖片貼上信號已在 FeedbackTab 內部直接處理,不需要外部連接
def cleanup(self) -> None:
"""清理資源"""
if self.command_tab:
self.command_tab.cleanup()
debug_log("分頁管理器清理完成")
def set_layout_mode(self, combined_mode: bool) -> None:
"""設置佈局模式"""
self.combined_mode = combined_mode
if self.settings_tab:
self.settings_tab.set_layout_mode(combined_mode)
def set_layout_orientation(self, orientation: str) -> None:
"""設置佈局方向"""
self.layout_orientation = orientation
if self.settings_tab:
self.settings_tab.set_layout_orientation(orientation)
def _save_splitter_position(self, splitter: QSplitter, config_key: str) -> None:
"""保存分割器位置"""
sizes = splitter.sizes()
self.config_manager.set_splitter_sizes(config_key, sizes)
debug_log(f"分割器位置保存成功,大小: {sizes}")

View File

@@ -0,0 +1,360 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
國際化支援模組
===============
提供統一的多語系支援功能,支援繁體中文、英文等語言。
自動偵測系統語言,並提供語言切換功能。
新架構:
- 使用分離的 JSON 翻譯檔案
- 支援巢狀翻譯鍵值
- 元資料支援
- 易於擴充新語言
作者: Minidoracat
"""
import os
import sys
import locale
import json
from typing import Dict, Any, Optional, Union
from pathlib import Path
from .debug import i18n_debug_log as debug_log
class I18nManager:
"""國際化管理器 - 新架構版本"""
def __init__(self):
self._current_language = None
self._translations = {}
self._supported_languages = ['zh-TW', 'en', 'zh-CN']
self._fallback_language = 'en'
self._config_file = self._get_config_file_path()
self._locales_dir = Path(__file__).parent / "gui" / "locales"
# 載入翻譯
self._load_all_translations()
# 設定語言
self._current_language = self._detect_language()
def _get_config_file_path(self) -> Path:
"""獲取配置文件路徑"""
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir / "language.json"
def _load_all_translations(self) -> None:
"""載入所有語言的翻譯檔案"""
self._translations = {}
for lang_code in self._supported_languages:
lang_dir = self._locales_dir / lang_code
translation_file = lang_dir / "translations.json"
if translation_file.exists():
try:
with open(translation_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self._translations[lang_code] = data
debug_log(f"成功載入語言 {lang_code}: {data.get('meta', {}).get('displayName', lang_code)}")
except Exception as e:
debug_log(f"載入語言檔案失敗 {lang_code}: {e}")
# 如果載入失敗,使用空的翻譯
self._translations[lang_code] = {}
else:
debug_log(f"找不到語言檔案: {translation_file}")
self._translations[lang_code] = {}
def _detect_language(self) -> str:
"""自動偵測語言"""
# 1. 優先使用用戶保存的語言設定
saved_lang = self._load_saved_language()
if saved_lang and saved_lang in self._supported_languages:
return saved_lang
# 2. 檢查環境變數
env_lang = os.getenv('MCP_LANGUAGE', '').strip()
if env_lang and env_lang in self._supported_languages:
return env_lang
# 3. 自動偵測系統語言
try:
# 獲取系統語言
system_locale = locale.getdefaultlocale()[0]
if system_locale:
if system_locale.startswith('zh_TW') or system_locale.startswith('zh_Hant'):
return 'zh-TW'
elif system_locale.startswith('zh_CN') or system_locale.startswith('zh_Hans'):
return 'zh-CN'
elif system_locale.startswith('en'):
return 'en'
except Exception:
pass
# 4. 回退到默認語言
return self._fallback_language
def _load_saved_language(self) -> Optional[str]:
"""載入保存的語言設定"""
try:
if self._config_file.exists():
with open(self._config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get('language')
except Exception:
pass
return None
def save_language(self, language: str) -> None:
"""保存語言設定"""
try:
config = {'language': language}
with open(self._config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
except Exception:
pass
def get_current_language(self) -> str:
"""獲取當前語言"""
return self._current_language
def set_language(self, language: str) -> bool:
"""設定語言"""
if language in self._supported_languages:
self._current_language = language
self.save_language(language)
return True
return False
def get_supported_languages(self) -> list:
"""獲取支援的語言列表"""
return self._supported_languages.copy()
def get_language_info(self, language_code: str) -> Dict[str, Any]:
"""獲取語言的元資料信息"""
if language_code in self._translations:
return self._translations[language_code].get('meta', {})
return {}
def _get_nested_value(self, data: Dict[str, Any], key_path: str) -> Optional[str]:
"""從巢狀字典中獲取值,支援點分隔的鍵路徑"""
keys = key_path.split('.')
current = data
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return None
return current if isinstance(current, str) else None
def t(self, key: str, **kwargs) -> str:
"""
翻譯函數 - 支援新舊兩種鍵值格式
新格式: 'buttons.submit' -> data['buttons']['submit']
舊格式: 'btn_submit_feedback' -> 兼容舊的鍵值
"""
# 獲取當前語言的翻譯
current_translations = self._translations.get(self._current_language, {})
# 嘗試新格式(巢狀鍵)
text = self._get_nested_value(current_translations, key)
# 如果沒有找到,嘗試舊格式的兼容映射
if text is None:
text = self._get_legacy_translation(current_translations, key)
# 如果還是沒有找到,嘗試使用回退語言
if text is None:
fallback_translations = self._translations.get(self._fallback_language, {})
text = self._get_nested_value(fallback_translations, key)
if text is None:
text = self._get_legacy_translation(fallback_translations, key)
# 最後回退到鍵本身
if text is None:
text = key
# 處理格式化參數
if kwargs:
try:
text = text.format(**kwargs)
except (KeyError, ValueError):
pass
return text
def _get_legacy_translation(self, translations: Dict[str, Any], key: str) -> Optional[str]:
"""獲取舊格式翻譯的兼容方法"""
# 舊鍵到新鍵的映射
legacy_mapping = {
# 應用程式
'app_title': 'app.title',
'project_directory': 'app.projectDirectory',
'language': 'app.language',
'settings': 'app.settings',
# 分頁
'feedback_tab': 'tabs.feedback',
'command_tab': 'tabs.command',
'images_tab': 'tabs.images',
# 回饋
'feedback_title': 'feedback.title',
'feedback_description': 'feedback.description',
'feedback_placeholder': 'feedback.placeholder',
# 命令
'command_title': 'command.title',
'command_description': 'command.description',
'command_placeholder': 'command.placeholder',
'command_output': 'command.output',
# 圖片
'images_title': 'images.title',
'images_select': 'images.select',
'images_paste': 'images.paste',
'images_clear': 'images.clear',
'images_status': 'images.status',
'images_status_with_size': 'images.statusWithSize',
'images_drag_hint': 'images.dragHint',
'images_delete_confirm': 'images.deleteConfirm',
'images_delete_title': 'images.deleteTitle',
'images_size_warning': 'images.sizeWarning',
'images_format_error': 'images.formatError',
# 按鈕
'submit': 'buttons.submit',
'cancel': 'buttons.cancel',
'close': 'buttons.close',
'clear': 'buttons.clear',
'btn_submit_feedback': 'buttons.submitFeedback',
'btn_cancel': 'buttons.cancel',
'btn_select_files': 'buttons.selectFiles',
'btn_paste_clipboard': 'buttons.pasteClipboard',
'btn_clear_all': 'buttons.clearAll',
'btn_run_command': 'buttons.runCommand',
# 狀態
'feedback_submitted': 'status.feedbackSubmitted',
'feedback_cancelled': 'status.feedbackCancelled',
'timeout_message': 'status.timeoutMessage',
'error_occurred': 'status.errorOccurred',
'loading': 'status.loading',
'connecting': 'status.connecting',
'connected': 'status.connected',
'disconnected': 'status.disconnected',
'uploading': 'status.uploading',
'upload_success': 'status.uploadSuccess',
'upload_failed': 'status.uploadFailed',
'command_running': 'status.commandRunning',
'command_finished': 'status.commandFinished',
'paste_success': 'status.pasteSuccess',
'paste_failed': 'status.pasteFailed',
'invalid_file_type': 'status.invalidFileType',
'file_too_large': 'status.fileTooLarge',
# 其他
'ai_summary': 'aiSummary',
'language_selector': 'languageSelector',
'language_zh_tw': 'languageNames.zhTw',
'language_en': 'languageNames.en',
'language_zh_cn': 'languageNames.zhCn',
# 測試
'test_qt_gui_summary': 'test.qtGuiSummary',
'test_web_ui_summary': 'test.webUiSummary',
}
# 檢查是否有對應的新鍵
new_key = legacy_mapping.get(key)
if new_key:
return self._get_nested_value(translations, new_key)
return None
def get_language_display_name(self, language_code: str) -> str:
"""獲取語言的顯示名稱"""
# 直接從當前語言的翻譯中獲取,避免遞歸
current_translations = self._translations.get(self._current_language, {})
# 根據語言代碼構建鍵值
lang_key = None
if language_code == 'zh-TW':
lang_key = 'languageNames.zhTw'
elif language_code == 'zh-CN':
lang_key = 'languageNames.zhCn'
elif language_code == 'en':
lang_key = 'languageNames.en'
else:
# 通用格式
lang_key = f"languageNames.{language_code.replace('-', '').lower()}"
# 直接獲取翻譯,避免調用 self.t() 產生遞歸
if lang_key:
display_name = self._get_nested_value(current_translations, lang_key)
if display_name:
return display_name
# 回退到元資料中的顯示名稱
meta = self.get_language_info(language_code)
return meta.get('displayName', language_code)
def reload_translations(self) -> None:
"""重新載入所有翻譯檔案(開發時使用)"""
self._load_all_translations()
def add_language(self, language_code: str, translation_file_path: str) -> bool:
"""動態添加新語言支援"""
try:
translation_file = Path(translation_file_path)
if not translation_file.exists():
return False
with open(translation_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self._translations[language_code] = data
if language_code not in self._supported_languages:
self._supported_languages.append(language_code)
debug_log(f"成功添加語言 {language_code}: {data.get('meta', {}).get('displayName', language_code)}")
return True
except Exception as e:
debug_log(f"添加語言失敗 {language_code}: {e}")
return False
# 全域的國際化管理器實例
_i18n_manager = None
def get_i18n_manager() -> I18nManager:
"""獲取全域的國際化管理器實例"""
global _i18n_manager
if _i18n_manager is None:
_i18n_manager = I18nManager()
return _i18n_manager
def t(key: str, **kwargs) -> str:
"""便捷的翻譯函數"""
return get_i18n_manager().t(key, **kwargs)
def set_language(language: str) -> bool:
"""設定語言"""
return get_i18n_manager().set_language(language)
def get_current_language() -> str:
"""獲取當前語言"""
return get_i18n_manager().get_current_language()
def reload_translations() -> None:
"""重新載入翻譯(開發用)"""
get_i18n_manager().reload_translations()

View File

@@ -0,0 +1,675 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP 伺服器主程式
================
MCP Feedback Enhanced 的核心伺服器程式,提供用戶互動回饋功能。
支援智能環境檢測,自動選擇 Qt GUI 或 Web UI 介面。
主要功能:
- 環境檢測(本地/遠端)
- 介面選擇GUI/Web UI
- 圖片處理和 MCP 整合
- 回饋結果標準化
作者: Fábio Ferreira (原作者)
增強: Minidoracat (Web UI, 圖片支援, 環境檢測)
重構: 模塊化設計
"""
import os
import sys
import json
import tempfile
import asyncio
import base64
from typing import Annotated, List
import io
from fastmcp import FastMCP, Image as MCPImage
from mcp.types import TextContent
from pydantic import Field
# 導入多語系支援
from .i18n import get_i18n_manager
# 導入統一的調試功能
from .debug import server_debug_log as debug_log
# ===== 編碼初始化 =====
def init_encoding():
"""初始化編碼設置,確保正確處理中文字符"""
try:
# Windows 特殊處理
if sys.platform == 'win32':
import msvcrt
# 設置為二進制模式
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
# 重新包裝為 UTF-8 文本流,並禁用緩衝
sys.stdin = io.TextIOWrapper(
sys.stdin.detach(),
encoding='utf-8',
errors='replace',
newline=None
)
sys.stdout = io.TextIOWrapper(
sys.stdout.detach(),
encoding='utf-8',
errors='replace',
newline='',
write_through=True # 關鍵:禁用寫入緩衝
)
else:
# 非 Windows 系統的標準設置
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stdin, 'reconfigure'):
sys.stdin.reconfigure(encoding='utf-8', errors='replace')
# 設置 stderr 編碼(用於調試訊息)
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
return True
except Exception as e:
# 如果編碼設置失敗,嘗試基本設置
try:
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stdin, 'reconfigure'):
sys.stdin.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except:
pass
return False
# 初始化編碼(在導入時就執行)
_encoding_initialized = init_encoding()
# ===== 常數定義 =====
SERVER_NAME = "互動式回饋收集 MCP"
SSH_ENV_VARS = ['SSH_CONNECTION', 'SSH_CLIENT', 'SSH_TTY']
REMOTE_ENV_VARS = ['REMOTE_CONTAINERS', 'CODESPACES']
# 初始化 MCP 服務器
from . import __version__
# 確保 log_level 設定為正確的大寫格式
fastmcp_settings = {}
# 檢查環境變數並設定正確的 log_level
env_log_level = os.getenv("FASTMCP_LOG_LEVEL", "").upper()
if env_log_level in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
fastmcp_settings["log_level"] = env_log_level
else:
# 預設使用 INFO 等級
fastmcp_settings["log_level"] = "INFO"
mcp = FastMCP(SERVER_NAME, version=__version__, **fastmcp_settings)
# ===== 工具函數 =====
def is_wsl_environment() -> bool:
"""
檢測是否在 WSL (Windows Subsystem for Linux) 環境中運行
Returns:
bool: True 表示 WSL 環境False 表示其他環境
"""
try:
# 檢查 /proc/version 文件是否包含 WSL 標識
if os.path.exists('/proc/version'):
with open('/proc/version', 'r') as f:
version_info = f.read().lower()
if 'microsoft' in version_info or 'wsl' in version_info:
debug_log("偵測到 WSL 環境(通過 /proc/version")
return True
# 檢查 WSL 相關環境變數
wsl_env_vars = ['WSL_DISTRO_NAME', 'WSL_INTEROP', 'WSLENV']
for env_var in wsl_env_vars:
if os.getenv(env_var):
debug_log(f"偵測到 WSL 環境變數: {env_var}")
return True
# 檢查是否存在 WSL 特有的路徑
wsl_paths = ['/mnt/c', '/mnt/d', '/proc/sys/fs/binfmt_misc/WSLInterop']
for path in wsl_paths:
if os.path.exists(path):
debug_log(f"偵測到 WSL 特有路徑: {path}")
return True
except Exception as e:
debug_log(f"WSL 檢測過程中發生錯誤: {e}")
return False
def is_remote_environment() -> bool:
"""
檢測是否在遠端環境中運行
Returns:
bool: True 表示遠端環境False 表示本地環境
"""
# WSL 不應被視為遠端環境,因為它可以訪問 Windows 瀏覽器
if is_wsl_environment():
debug_log("WSL 環境不被視為遠端環境")
return False
# 檢查 SSH 連線指標
for env_var in SSH_ENV_VARS:
if os.getenv(env_var):
debug_log(f"偵測到 SSH 環境變數: {env_var}")
return True
# 檢查遠端開發環境
for env_var in REMOTE_ENV_VARS:
if os.getenv(env_var):
debug_log(f"偵測到遠端開發環境: {env_var}")
return True
# 檢查 Docker 容器
if os.path.exists('/.dockerenv'):
debug_log("偵測到 Docker 容器環境")
return True
# Windows 遠端桌面檢查
if sys.platform == 'win32':
session_name = os.getenv('SESSIONNAME', '')
if session_name and 'RDP' in session_name:
debug_log(f"偵測到 Windows 遠端桌面: {session_name}")
return True
# Linux 無顯示環境檢查(但排除 WSL
if sys.platform.startswith('linux') and not os.getenv('DISPLAY') and not is_wsl_environment():
debug_log("偵測到 Linux 無顯示環境")
return True
return False
def can_use_gui() -> bool:
"""
檢測是否可以使用圖形介面
Returns:
bool: True 表示可以使用 GUIFalse 表示只能使用 Web UI
"""
if is_remote_environment():
return False
try:
from PySide6.QtWidgets import QApplication
debug_log("成功載入 PySide6可使用 GUI")
return True
except ImportError:
debug_log("無法載入 PySide6使用 Web UI")
return False
except Exception as e:
debug_log(f"GUI 初始化失敗: {e}")
return False
def save_feedback_to_file(feedback_data: dict, file_path: str = None) -> str:
"""
將回饋資料儲存到 JSON 文件
Args:
feedback_data: 回饋資料字典
file_path: 儲存路徑,若為 None 則自動產生臨時文件
Returns:
str: 儲存的文件路徑
"""
if file_path is None:
temp_fd, file_path = tempfile.mkstemp(suffix='.json', prefix='feedback_')
os.close(temp_fd)
# 確保目錄存在
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)
# 複製數據以避免修改原始數據
json_data = feedback_data.copy()
# 處理圖片數據:將 bytes 轉換為 base64 字符串以便 JSON 序列化
if "images" in json_data and isinstance(json_data["images"], list):
processed_images = []
for img in json_data["images"]:
if isinstance(img, dict) and "data" in img:
processed_img = img.copy()
# 如果 data 是 bytes轉換為 base64 字符串
if isinstance(img["data"], bytes):
processed_img["data"] = base64.b64encode(img["data"]).decode('utf-8')
processed_img["data_type"] = "base64"
processed_images.append(processed_img)
else:
processed_images.append(img)
json_data["images"] = processed_images
# 儲存資料
with open(file_path, "w", encoding="utf-8") as f:
json.dump(json_data, f, ensure_ascii=False, indent=2)
debug_log(f"回饋資料已儲存至: {file_path}")
return file_path
def create_feedback_text(feedback_data: dict) -> str:
"""
建立格式化的回饋文字
Args:
feedback_data: 回饋資料字典
Returns:
str: 格式化後的回饋文字
"""
text_parts = []
# 基本回饋內容
if feedback_data.get("interactive_feedback"):
text_parts.append(f"=== 用戶回饋 ===\n{feedback_data['interactive_feedback']}")
# 命令執行日誌
if feedback_data.get("command_logs"):
text_parts.append(f"=== 命令執行日誌 ===\n{feedback_data['command_logs']}")
# 圖片附件概要
if feedback_data.get("images"):
images = feedback_data["images"]
text_parts.append(f"=== 圖片附件概要 ===\n用戶提供了 {len(images)} 張圖片:")
for i, img in enumerate(images, 1):
size = img.get("size", 0)
name = img.get("name", "unknown")
# 智能單位顯示
if size < 1024:
size_str = f"{size} B"
elif size < 1024 * 1024:
size_kb = size / 1024
size_str = f"{size_kb:.1f} KB"
else:
size_mb = size / (1024 * 1024)
size_str = f"{size_mb:.1f} MB"
img_info = f" {i}. {name} ({size_str})"
# 為提高兼容性,添加 base64 預覽信息
if img.get("data"):
try:
if isinstance(img["data"], bytes):
img_base64 = base64.b64encode(img["data"]).decode('utf-8')
elif isinstance(img["data"], str):
img_base64 = img["data"]
else:
img_base64 = None
if img_base64:
# 只顯示前50個字符的預覽
preview = img_base64[:50] + "..." if len(img_base64) > 50 else img_base64
img_info += f"\n Base64 預覽: {preview}"
img_info += f"\n 完整 Base64 長度: {len(img_base64)} 字符"
# 如果 AI 助手不支援 MCP 圖片,可以提供完整 base64
debug_log(f"圖片 {i} Base64 已準備,長度: {len(img_base64)}")
# 檢查是否啟用 Base64 詳細模式(從 UI 設定中獲取)
include_full_base64 = feedback_data.get("settings", {}).get("enable_base64_detail", False)
if include_full_base64:
# 根據檔案名推斷 MIME 類型
file_name = img.get("name", "image.png")
if file_name.lower().endswith(('.jpg', '.jpeg')):
mime_type = 'image/jpeg'
elif file_name.lower().endswith('.gif'):
mime_type = 'image/gif'
elif file_name.lower().endswith('.webp'):
mime_type = 'image/webp'
else:
mime_type = 'image/png'
img_info += f"\n 完整 Base64: data:{mime_type};base64,{img_base64}"
except Exception as e:
debug_log(f"圖片 {i} Base64 處理失敗: {e}")
text_parts.append(img_info)
# 添加兼容性說明
text_parts.append("\n💡 注意:如果 AI 助手無法顯示圖片,圖片數據已包含在上述 Base64 信息中。")
return "\n\n".join(text_parts) if text_parts else "用戶未提供任何回饋內容。"
def process_images(images_data: List[dict]) -> List[MCPImage]:
"""
處理圖片資料,轉換為 MCP 圖片對象
Args:
images_data: 圖片資料列表
Returns:
List[MCPImage]: MCP 圖片對象列表
"""
mcp_images = []
for i, img in enumerate(images_data, 1):
try:
if not img.get("data"):
debug_log(f"圖片 {i} 沒有資料,跳過")
continue
# 檢查數據類型並相應處理
if isinstance(img["data"], bytes):
# 如果是原始 bytes 數據,直接使用
image_bytes = img["data"]
debug_log(f"圖片 {i} 使用原始 bytes 數據,大小: {len(image_bytes)} bytes")
elif isinstance(img["data"], str):
# 如果是 base64 字符串,進行解碼
image_bytes = base64.b64decode(img["data"])
debug_log(f"圖片 {i} 從 base64 解碼,大小: {len(image_bytes)} bytes")
else:
debug_log(f"圖片 {i} 數據類型不支援: {type(img['data'])}")
continue
if len(image_bytes) == 0:
debug_log(f"圖片 {i} 數據為空,跳過")
continue
# 根據文件名推斷格式
file_name = img.get("name", "image.png")
if file_name.lower().endswith(('.jpg', '.jpeg')):
image_format = 'jpeg'
elif file_name.lower().endswith('.gif'):
image_format = 'gif'
else:
image_format = 'png' # 默認使用 PNG
# 創建 MCPImage 對象
mcp_image = MCPImage(data=image_bytes, format=image_format)
mcp_images.append(mcp_image)
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()}")
debug_log(f"共處理 {len(mcp_images)} 張圖片")
return mcp_images
async def launch_gui_with_timeout(project_dir: str, summary: str, timeout: int) -> dict:
"""
啟動 GUI 模式並處理超時
"""
debug_log(f"啟動 GUI 模式(超時:{timeout}秒)")
try:
from .gui import feedback_ui_with_timeout
# 直接調用帶超時的 GUI 函數
result = feedback_ui_with_timeout(project_dir, summary, timeout)
if result:
return {
"logs": f"GUI 模式回饋收集完成",
"interactive_feedback": result.get("interactive_feedback", ""),
"images": result.get("images", [])
}
else:
return {
"logs": "用戶取消了回饋收集",
"interactive_feedback": "",
"images": []
}
except TimeoutError as e:
# 超時異常 - 這是預期的行為
raise e
except Exception as e:
debug_log(f"GUI 啟動失败: {e}")
raise Exception(f"GUI 啟動失败: {e}")
# ===== MCP 工具定義 =====
@mcp.tool()
async def interactive_feedback(
project_directory: Annotated[str, Field(description="專案目錄路徑")] = ".",
summary: Annotated[str, Field(description="AI 工作完成的摘要說明")] = "我已完成了您請求的任務。",
timeout: Annotated[int, Field(description="等待用戶回饋的超時時間(秒)")] = 600
) -> List:
"""
收集用戶的互動回饋,支援文字和圖片
此工具會自動偵測運行環境:
- 遠端環境:使用 Web UI
- 本地環境:使用 Qt GUI
- 可透過 FORCE_WEB 環境變數強制使用 Web UI
用戶可以:
1. 執行命令來驗證結果
2. 提供文字回饋
3. 上傳圖片作為回饋
4. 查看 AI 的工作摘要
介面控制(按優先級排序):
1. **FORCE_WEB 環境變數**:在 mcp.json 中設置 "FORCE_WEB": "true"
2. 自動檢測:根據運行環境自動選擇
調試模式:
- 設置環境變數 MCP_DEBUG=true 可啟用詳細調試輸出
- 生產環境建議關閉調試模式以避免輸出干擾
Args:
project_directory: 專案目錄路徑
summary: AI 工作完成的摘要說明
timeout: 等待用戶回饋的超時時間(秒),預設為 600 秒10 分鐘)
Returns:
List: 包含 TextContent 和 MCPImage 對象的列表
"""
# 檢查環境變數 FORCE_WEB
force_web = False
env_force_web = os.getenv("FORCE_WEB", "").lower()
if env_force_web in ("true", "1", "yes", "on"):
force_web = True
debug_log("環境變數 FORCE_WEB 已啟用,強制使用 Web UI")
elif env_force_web in ("false", "0", "no", "off"):
force_web = False
debug_log("環境變數 FORCE_WEB 已停用,使用預設邏輯")
# 環境偵測
is_remote = is_remote_environment()
can_gui = can_use_gui()
use_web_ui = is_remote or not can_gui or force_web
debug_log(f"環境偵測結果 - 遠端: {is_remote}, GUI 可用: {can_gui}, 強制 Web UI: {force_web}")
debug_log(f"決定使用介面: {'Web UI' if use_web_ui else 'Qt GUI'}")
try:
# 確保專案目錄存在
if not os.path.exists(project_directory):
project_directory = os.getcwd()
project_directory = os.path.abspath(project_directory)
# 選擇適當的介面
if use_web_ui:
result = await launch_web_ui_with_timeout(project_directory, summary, timeout)
else:
result = await launch_gui_with_timeout(project_directory, summary, timeout)
# 處理取消情況
if not result:
return [TextContent(type="text", text="用戶取消了回饋。")]
# 儲存詳細結果
save_feedback_to_file(result)
# 建立回饋項目列表
feedback_items = []
# 添加文字回饋
if result.get("interactive_feedback") or result.get("command_logs") or result.get("images"):
feedback_text = create_feedback_text(result)
feedback_items.append(TextContent(type="text", text=feedback_text))
debug_log("文字回饋已添加")
# 添加圖片回饋
if result.get("images"):
mcp_images = process_images(result["images"])
feedback_items.extend(mcp_images)
debug_log(f"已添加 {len(mcp_images)} 張圖片")
# 確保至少有一個回饋項目
if not feedback_items:
feedback_items.append(TextContent(type="text", text="用戶未提供任何回饋內容。"))
debug_log(f"回饋收集完成,共 {len(feedback_items)} 個項目")
return feedback_items
except Exception as e:
error_msg = f"回饋收集錯誤: {str(e)}"
debug_log(f"錯誤: {error_msg}")
return [TextContent(type="text", text=error_msg)]
async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: int) -> dict:
"""
啟動 Web UI 收集回饋,支援自訂超時時間
Args:
project_dir: 專案目錄路徑
summary: AI 工作摘要
timeout: 超時時間(秒)
Returns:
dict: 收集到的回饋資料
"""
debug_log(f"啟動 Web UI 介面,超時時間: {timeout}")
try:
# 使用新的 web 模組
from .web import launch_web_feedback_ui, stop_web_ui
# 傳遞 timeout 參數給 Web UI
return await launch_web_feedback_ui(project_dir, summary, timeout)
except ImportError as e:
debug_log(f"無法導入 Web UI 模組: {e}")
return {
"command_logs": "",
"interactive_feedback": f"Web UI 模組導入失敗: {str(e)}",
"images": []
}
except TimeoutError as e:
debug_log(f"Web UI 超時: {e}")
# 超時時確保停止 Web 服務器
try:
from .web import stop_web_ui
stop_web_ui()
debug_log("Web UI 服務器已因超時而停止")
except Exception as stop_error:
debug_log(f"停止 Web UI 服務器時發生錯誤: {stop_error}")
return {
"command_logs": "",
"interactive_feedback": f"回饋收集超時({timeout}秒),介面已自動關閉。",
"images": []
}
except Exception as e:
error_msg = f"Web UI 錯誤: {e}"
debug_log(f"{error_msg}")
# 發生錯誤時也要停止 Web 服務器
try:
from .web import stop_web_ui
stop_web_ui()
debug_log("Web UI 服務器已因錯誤而停止")
except Exception as stop_error:
debug_log(f"停止 Web UI 服務器時發生錯誤: {stop_error}")
return {
"command_logs": "",
"interactive_feedback": f"錯誤: {str(e)}",
"images": []
}
@mcp.tool()
def get_system_info() -> str:
"""
獲取系統環境資訊
Returns:
str: JSON 格式的系統資訊
"""
is_remote = is_remote_environment()
is_wsl = is_wsl_environment()
can_gui = can_use_gui()
system_info = {
"平台": sys.platform,
"Python 版本": sys.version.split()[0],
"WSL 環境": is_wsl,
"遠端環境": is_remote,
"GUI 可用": can_gui,
"建議介面": "Web UI" if is_remote or not can_gui else "Qt GUI",
"環境變數": {
"SSH_CONNECTION": os.getenv("SSH_CONNECTION"),
"SSH_CLIENT": os.getenv("SSH_CLIENT"),
"DISPLAY": os.getenv("DISPLAY"),
"VSCODE_INJECTION": os.getenv("VSCODE_INJECTION"),
"SESSIONNAME": os.getenv("SESSIONNAME"),
"WSL_DISTRO_NAME": os.getenv("WSL_DISTRO_NAME"),
"WSL_INTEROP": os.getenv("WSL_INTEROP"),
"WSLENV": os.getenv("WSLENV"),
}
}
return json.dumps(system_info, ensure_ascii=False, indent=2)
# ===== 主程式入口 =====
def main():
"""主要入口點,用於套件執行"""
# 檢查是否啟用調試模式
debug_enabled = os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on")
if debug_enabled:
debug_log("🚀 啟動互動式回饋收集 MCP 服務器")
debug_log(f" 服務器名稱: {SERVER_NAME}")
debug_log(f" 版本: {__version__}")
debug_log(f" 平台: {sys.platform}")
debug_log(f" 編碼初始化: {'成功' if _encoding_initialized else '失敗'}")
debug_log(f" 遠端環境: {is_remote_environment()}")
debug_log(f" GUI 可用: {can_use_gui()}")
debug_log(f" 建議介面: {'Web UI' if is_remote_environment() or not can_use_gui() else 'Qt GUI'}")
debug_log(" 等待來自 AI 助手的調用...")
debug_log("準備啟動 MCP 伺服器...")
debug_log("調用 mcp.run()...")
try:
# 使用正確的 FastMCP API
mcp.run()
except KeyboardInterrupt:
if debug_enabled:
debug_log("收到中斷信號,正常退出")
sys.exit(0)
except Exception as e:
if debug_enabled:
debug_log(f"MCP 服務器啟動失敗: {e}")
import traceback
debug_log(f"詳細錯誤: {traceback.format_exc()}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,286 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP 增強測試系統
================
完整的 MCP 測試框架,模擬真實的 Cursor IDE 調用場景。
主要功能:
- 真實 MCP 調用模擬
- 完整的回饋循環測試
- 多場景測試覆蓋
- 詳細的測試報告
使用方法:
python -m mcp_feedback_enhanced.test_mcp_enhanced
python -m mcp_feedback_enhanced.test_mcp_enhanced --scenario basic_workflow
python -m mcp_feedback_enhanced.test_mcp_enhanced --tags quick
"""
import asyncio
import argparse
import sys
import os
from typing import List, Optional
from pathlib import Path
# 添加專案根目錄到 Python 路徑
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from .testing import TestScenarios, TestReporter, TestConfig, DEFAULT_CONFIG
from .debug import debug_log
class MCPTestRunner:
"""MCP 測試運行器"""
def __init__(self, config: Optional[TestConfig] = None):
self.config = config or DEFAULT_CONFIG
self.scenarios = TestScenarios(self.config)
self.reporter = TestReporter(self.config)
async def run_single_scenario(self, scenario_name: str) -> bool:
"""運行單個測試場景"""
debug_log(f"🎯 運行單個測試場景: {scenario_name}")
result = await self.scenarios.run_scenario(scenario_name)
# 生成報告
test_results = {
"success": result.get("success", False),
"total_scenarios": 1,
"passed_scenarios": 1 if result.get("success", False) else 0,
"failed_scenarios": 0 if result.get("success", False) else 1,
"results": [result]
}
report = self.reporter.generate_report(test_results)
self.reporter.print_summary(report)
# 保存報告
if self.config.report_output_dir:
report_path = self.reporter.save_report(report)
debug_log(f"📄 詳細報告已保存: {report_path}")
return result.get("success", False)
async def run_scenarios_by_tags(self, tags: List[str]) -> bool:
"""根據標籤運行測試場景"""
debug_log(f"🏷️ 運行標籤測試: {', '.join(tags)}")
results = await self.scenarios.run_all_scenarios(tags)
# 生成報告
report = self.reporter.generate_report(results)
self.reporter.print_summary(report)
# 保存報告
if self.config.report_output_dir:
report_path = self.reporter.save_report(report)
debug_log(f"📄 詳細報告已保存: {report_path}")
return results.get("success", False)
async def run_all_scenarios(self) -> bool:
"""運行所有測試場景"""
debug_log("🚀 運行所有測試場景")
results = await self.scenarios.run_all_scenarios()
# 生成報告
report = self.reporter.generate_report(results)
self.reporter.print_summary(report)
# 保存報告
if self.config.report_output_dir:
report_path = self.reporter.save_report(report)
debug_log(f"📄 詳細報告已保存: {report_path}")
return results.get("success", False)
def list_scenarios(self, tags: Optional[List[str]] = None):
"""列出可用的測試場景"""
scenarios = self.scenarios.list_scenarios(tags)
print("\n📋 可用的測試場景:")
print("=" * 50)
for scenario in scenarios:
tags_str = f" [{', '.join(scenario.tags)}]" if scenario.tags else ""
print(f"🧪 {scenario.name}{tags_str}")
print(f" {scenario.description}")
print(f" 超時: {scenario.timeout}s")
print()
print(f"總計: {len(scenarios)} 個測試場景")
def create_config_from_args(args) -> TestConfig:
"""從命令行參數創建配置"""
config = TestConfig.from_env()
# 覆蓋命令行參數
if args.timeout:
config.test_timeout = args.timeout
if args.verbose is not None:
config.test_verbose = args.verbose
if args.debug:
config.test_debug = True
os.environ["MCP_DEBUG"] = "true"
if args.report_format:
config.report_format = args.report_format
if args.report_dir:
config.report_output_dir = args.report_dir
if args.project_dir:
config.test_project_dir = args.project_dir
return config
async def main():
"""主函數"""
parser = argparse.ArgumentParser(
description="MCP 增強測試系統",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例用法:
%(prog)s # 運行所有測試
%(prog)s --scenario basic_workflow # 運行特定場景
%(prog)s --tags quick # 運行快速測試
%(prog)s --tags basic,integration # 運行多個標籤
%(prog)s --list # 列出所有場景
%(prog)s --debug --verbose # 調試模式
"""
)
# 測試選項
parser.add_argument(
'--scenario',
help='運行特定的測試場景'
)
parser.add_argument(
'--tags',
help='根據標籤運行測試場景 (逗號分隔)'
)
parser.add_argument(
'--list',
action='store_true',
help='列出所有可用的測試場景'
)
# 配置選項
parser.add_argument(
'--timeout',
type=int,
help='測試超時時間 (秒)'
)
parser.add_argument(
'--verbose',
action='store_true',
help='詳細輸出'
)
parser.add_argument(
'--debug',
action='store_true',
help='調試模式'
)
parser.add_argument(
'--project-dir',
help='測試項目目錄'
)
# 報告選項
parser.add_argument(
'--report-format',
choices=['html', 'json', 'markdown'],
help='報告格式'
)
parser.add_argument(
'--report-dir',
help='報告輸出目錄'
)
args = parser.parse_args()
# 創建配置
config = create_config_from_args(args)
# 創建測試運行器
runner = MCPTestRunner(config)
try:
if args.list:
# 列出測試場景
tags = args.tags.split(',') if args.tags else None
runner.list_scenarios(tags)
return
success = False
if args.scenario:
# 運行特定場景
success = await runner.run_single_scenario(args.scenario)
elif args.tags:
# 根據標籤運行
tags = [tag.strip() for tag in args.tags.split(',')]
success = await runner.run_scenarios_by_tags(tags)
else:
# 運行所有場景
success = await runner.run_all_scenarios()
if success:
debug_log("🎉 所有測試通過!")
sys.exit(0)
else:
debug_log("❌ 部分測試失敗")
sys.exit(1)
except KeyboardInterrupt:
debug_log("\n⚠️ 測試被用戶中斷")
sys.exit(130)
except Exception as e:
debug_log(f"❌ 測試執行失敗: {e}")
if config.test_debug:
import traceback
debug_log(f"詳細錯誤: {traceback.format_exc()}")
sys.exit(1)
def run_quick_test():
"""快速測試入口"""
os.environ["MCP_DEBUG"] = "true"
# 設置快速測試配置
config = TestConfig.from_env()
config.test_timeout = 60
config.report_format = "markdown"
async def quick_test():
runner = MCPTestRunner(config)
return await runner.run_scenarios_by_tags(["quick"])
return asyncio.run(quick_test())
def run_basic_workflow_test():
"""基礎工作流程測試入口"""
os.environ["MCP_DEBUG"] = "true"
config = TestConfig.from_env()
config.test_timeout = 180
async def workflow_test():
runner = MCPTestRunner(config)
return await runner.run_single_scenario("basic_workflow")
return asyncio.run(workflow_test())
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Qt GUI 測試模組
===============
用於測試 MCP Feedback Enhanced 的 Qt GUI 功能。
包含完整的 GUI 功能測試。
功能測試:
- Qt GUI 界面啟動
- 多語言支援
- 圖片上傳功能
- 回饋提交功能
- 快捷鍵功能
使用方法:
python -m mcp_feedback_enhanced.test_qt_gui
作者: Minidoracat
"""
import sys
import os
from typing import Optional, Dict, Any
# 添加專案根目錄到 Python 路徑
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from .debug import debug_log
from .i18n import t
# 嘗試導入 Qt GUI 模組
try:
from .gui import feedback_ui
QT_GUI_AVAILABLE = True
except ImportError as e:
debug_log(f"⚠️ 無法導入 Qt GUI 模組: {e}")
QT_GUI_AVAILABLE = False
def test_qt_gui():
"""測試 Qt GUI 功能"""
try:
# 測試參數
project_directory = os.getcwd()
# 使用國際化系統獲取測試摘要
prompt = t('test.qtGuiSummary')
debug_log("🚀 啟動 Qt GUI 測試...")
debug_log("📝 測試項目:")
debug_log(" - 圖片預覽功能")
debug_log(" - X刪除按鈕")
debug_log(" - 視窗大小調整")
debug_log(" - 分割器調整")
debug_log(" - 智能 Ctrl+V 功能")
debug_log("")
# 啟動 GUI
result = feedback_ui(project_directory, prompt)
if result:
debug_log("\n✅ 測試完成!")
debug_log(f"📄 收到回饋: {result.get('interactive_feedback', '')}")
if result.get('images'):
debug_log(f"🖼️ 收到圖片: {len(result['images'])}")
if result.get('logs'):
debug_log(f"📋 命令日誌: {len(result['logs'])}")
else:
debug_log("\n❌ 測試取消或無回饋")
except ImportError as e:
debug_log(f"❌ 導入錯誤: {e}")
debug_log("請確保已安裝 PySide6: pip install PySide6")
return False
except Exception as e:
debug_log(f"❌ 測試錯誤: {e}")
return False
return True
if __name__ == "__main__":
debug_log("🧪 MCP Feedback Enhanced - Qt GUI 測試")
debug_log("=" * 50)
# 檢查環境
try:
from PySide6.QtWidgets import QApplication
debug_log("✅ PySide6 已安裝")
except ImportError:
debug_log("❌ PySide6 未安裝,請執行: pip install PySide6")
sys.exit(1)
# 運行測試
success = test_qt_gui()
if success:
debug_log("\n🎉 測試程序運行完成")
else:
debug_log("\n💥 測試程序運行失敗")
sys.exit(1)

View File

@@ -0,0 +1,375 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP Feedback Enhanced - Web UI 測試模組
========================================
用於測試 MCP Feedback Enhanced 的 Web UI 功能。
包含完整的 Web UI 功能測試。
功能測試:
- Web UI 服務器啟動
- 會話管理功能
- WebSocket 通訊
- 多語言支援
- 命令執行功能
使用方法:
python -m mcp_feedback_enhanced.test_web_ui
作者: Minidoracat
"""
import asyncio
import webbrowser
import time
import sys
import os
import socket
import threading
from pathlib import Path
from typing import Dict, Any, Optional
# 添加專案根目錄到 Python 路徑
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from .debug import debug_log
from .i18n import t
# 嘗試導入 Web UI 模組
try:
# 使用新的 web 模組
from .web import WebUIManager, launch_web_feedback_ui, get_web_ui_manager
from .web.utils.browser import smart_browser_open, is_wsl_environment
WEB_UI_AVAILABLE = True
debug_log("✅ 使用新的 web 模組")
except ImportError as e:
debug_log(f"⚠️ 無法導入 Web UI 模組: {e}")
WEB_UI_AVAILABLE = False
def get_test_summary():
"""獲取測試摘要,使用國際化系統"""
return t('test.webUiSummary')
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
def test_web_ui(keep_running=False):
"""Test the Web UI functionality"""
debug_log("🧪 測試 MCP Feedback Enhanced Web UI")
debug_log("=" * 50)
# Test import
try:
# 使用新的 web 模組
from .web import WebUIManager, launch_web_feedback_ui
debug_log("✅ Web UI 模組匯入成功")
except ImportError as e:
debug_log(f"❌ Web UI 模組匯入失敗: {e}")
return False, None
# Find free port
try:
free_port = find_free_port()
debug_log(f"🔍 找到可用端口: {free_port}")
except Exception as e:
debug_log(f"❌ 尋找可用端口失敗: {e}")
return False, None
# Test manager creation
try:
manager = WebUIManager(port=free_port)
debug_log("✅ WebUIManager 創建成功")
except Exception as e:
debug_log(f"❌ WebUIManager 創建失敗: {e}")
return False, None
# Test server start (with timeout)
server_started = False
try:
debug_log("🚀 啟動 Web 服務器...")
def start_server():
try:
manager.start_server()
return True
except Exception as e:
debug_log(f"服務器啟動錯誤: {e}")
return False
# Start server in thread
server_thread = threading.Thread(target=start_server)
server_thread.daemon = True
server_thread.start()
# Wait a moment and test if server is responsive
time.sleep(3)
# Test if port is listening
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(1)
result = s.connect_ex((manager.host, manager.port))
if result == 0:
server_started = True
debug_log("✅ Web 服務器啟動成功")
debug_log(f"🌐 服務器運行在: http://{manager.host}:{manager.port}")
else:
debug_log(f"❌ 無法連接到服務器端口 {manager.port}")
except Exception as e:
debug_log(f"❌ Web 服務器啟動失敗: {e}")
return False, None
if not server_started:
debug_log("❌ 服務器未能正常啟動")
return False, None
# Test session creation
session_info = None
try:
project_dir = str(Path.cwd())
# 使用國際化系統獲取測試摘要
summary = t('test.webUiSummary')
session_id = manager.create_session(project_dir, summary)
session_info = {
'manager': manager,
'session_id': session_id,
'url': f"http://{manager.host}:{manager.port}" # 使用根路徑
}
debug_log(f"✅ 測試會話創建成功 (ID: {session_id[:8]}...)")
debug_log(f"🔗 測試 URL: {session_info['url']}")
# 測試瀏覽器啟動功能
try:
debug_log("🌐 測試瀏覽器啟動功能...")
if is_wsl_environment():
debug_log("✅ 檢測到 WSL 環境,使用 WSL 專用瀏覽器啟動")
else:
debug_log(" 非 WSL 環境,使用標準瀏覽器啟動")
smart_browser_open(session_info['url'])
debug_log(f"✅ 瀏覽器啟動成功: {session_info['url']}")
except Exception as browser_error:
debug_log(f"⚠️ 瀏覽器啟動失敗: {browser_error}")
debug_log("💡 這可能是正常的,請手動在瀏覽器中開啟上述 URL")
except Exception as e:
debug_log(f"❌ 會話創建失敗: {e}")
return False, None
debug_log("\n" + "=" * 50)
debug_log("🎉 所有測試通過Web UI 準備就緒")
debug_log("📝 注意事項:")
debug_log(" - Web UI 會在 SSH remote 環境下自動啟用")
debug_log(" - 本地環境會繼續使用 Qt GUI")
debug_log(" - 支援即時命令執行和 WebSocket 通訊")
debug_log(" - 提供現代化的深色主題界面")
debug_log(" - 支援智能 Ctrl+V 圖片貼上功能")
return True, session_info
def test_environment_detection():
"""Test environment detection logic"""
debug_log("🔍 測試環境檢測功能")
debug_log("-" * 30)
try:
from .server import is_remote_environment, is_wsl_environment, can_use_gui
wsl_detected = is_wsl_environment()
remote_detected = is_remote_environment()
gui_available = can_use_gui()
debug_log(f"WSL 環境檢測: {'' if wsl_detected else ''}")
debug_log(f"遠端環境檢測: {'' if remote_detected else ''}")
debug_log(f"GUI 可用性: {'' if gui_available else ''}")
if wsl_detected:
debug_log("✅ 檢測到 WSL 環境,將使用 Web UI 並支援 Windows 瀏覽器啟動")
elif remote_detected:
debug_log("✅ 將使用 Web UI (適合遠端開發環境)")
else:
debug_log("✅ 將使用 Qt GUI (本地環境)")
return True
except Exception as e:
debug_log(f"❌ 環境檢測失敗: {e}")
return False
def test_mcp_integration():
"""Test MCP server integration"""
debug_log("\n🔧 測試 MCP 整合功能")
debug_log("-" * 30)
try:
from .server import interactive_feedback
debug_log("✅ MCP 工具函數可用")
# Test timeout parameter
debug_log("✅ 支援 timeout 參數")
# Test environment-based Web UI selection
debug_log("✅ 支援基於環境變數的 Web UI 選擇")
# Test would require actual MCP call, so just verify import
debug_log("✅ 準備接受來自 AI 助手的調用")
return True
except Exception as e:
debug_log(f"❌ MCP 整合測試失敗: {e}")
return False
def test_new_parameters():
"""Test timeout parameter and environment variable support"""
debug_log("\n🆕 測試參數功能")
debug_log("-" * 30)
try:
from .server import interactive_feedback
# 測試參數是否存在
import inspect
sig = inspect.signature(interactive_feedback)
# 檢查 timeout 參數
if 'timeout' in sig.parameters:
timeout_param = sig.parameters['timeout']
debug_log(f"✅ timeout 參數存在,預設值: {timeout_param.default}")
else:
debug_log("❌ timeout 參數不存在")
return False
# 檢查環境變數支援
import os
current_force_web = os.getenv("FORCE_WEB")
if current_force_web:
debug_log(f"✅ 檢測到 FORCE_WEB 環境變數: {current_force_web}")
else:
debug_log(" FORCE_WEB 環境變數未設定(將使用預設邏輯)")
debug_log("✅ 參數功能正常")
return True
except Exception as e:
debug_log(f"❌ 參數測試失敗: {e}")
return False
def test_environment_web_ui_mode():
"""Test environment-based Web UI mode"""
debug_log("\n🌐 測試環境變數控制 Web UI 模式")
debug_log("-" * 30)
try:
from .server import interactive_feedback, is_remote_environment, is_wsl_environment, can_use_gui
import os
# 顯示當前環境狀態
is_wsl = is_wsl_environment()
is_remote = is_remote_environment()
gui_available = can_use_gui()
force_web_env = os.getenv("FORCE_WEB", "").lower()
debug_log(f"當前環境 - WSL: {is_wsl}, 遠端: {is_remote}, GUI 可用: {gui_available}")
debug_log(f"FORCE_WEB 環境變數: {force_web_env or '未設定'}")
if force_web_env in ("true", "1", "yes", "on"):
debug_log("✅ FORCE_WEB 已啟用,將強制使用 Web UI")
elif is_wsl:
debug_log("✅ WSL 環境,將使用 Web UI 並支援 Windows 瀏覽器啟動")
elif not is_remote and gui_available:
debug_log(" 本地 GUI 環境,將使用 Qt GUI")
debug_log("💡 可設定 FORCE_WEB=true 強制使用 Web UI 進行測試")
else:
debug_log(" 將自動使用 Web UI遠端環境或 GUI 不可用)")
return True
except Exception as e:
debug_log(f"❌ 環境變數測試失敗: {e}")
return False
def interactive_demo(session_info):
"""Run interactive demo with the Web UI"""
debug_log(f"\n🌐 Web UI 互動測試模式")
debug_log("=" * 50)
debug_log(f"服務器地址: {session_info['url']}") # 簡化輸出,只顯示服務器地址
debug_log("\n📖 操作指南:")
debug_log(" 1. 在瀏覽器中開啟上面的服務器地址")
debug_log(" 2. 嘗試以下功能:")
debug_log(" - 點擊 '顯示命令區塊' 按鈕")
debug_log(" - 輸入命令如 'echo Hello World' 並執行")
debug_log(" - 在回饋區域輸入文字")
debug_log(" - 使用 Ctrl+Enter 提交回饋")
debug_log(" 3. 測試 WebSocket 即時通訊功能")
debug_log(" 4. 測試頁面持久性(提交反饋後頁面不關閉)")
debug_log("\n⌨️ 控制選項:")
debug_log(" - 按 Enter 繼續運行")
debug_log(" - 輸入 'q''quit' 停止服務器")
while True:
try:
user_input = input("\n>>> ").strip().lower()
if user_input in ['q', 'quit', 'exit']:
debug_log("🛑 停止服務器...")
break
elif user_input == '':
debug_log(f"🔄 服務器持續運行在: {session_info['url']}")
debug_log(" 瀏覽器應該仍可正常訪問")
else:
debug_log("❓ 未知命令。按 Enter 繼續運行,或輸入 'q' 退出")
except KeyboardInterrupt:
debug_log("\n🛑 收到中斷信號,停止服務器...")
break
debug_log("✅ Web UI 測試完成")
if __name__ == "__main__":
debug_log("MCP Feedback Enhanced - Web UI 測試")
debug_log("=" * 60)
# Test environment detection
env_test = test_environment_detection()
# Test new parameters
params_test = test_new_parameters()
# Test environment-based Web UI mode
env_web_test = test_environment_web_ui_mode()
# Test MCP integration
mcp_test = test_mcp_integration()
# Test Web UI
web_test, session_info = test_web_ui()
debug_log("\n" + "=" * 60)
if env_test and params_test and env_web_test and mcp_test and web_test:
debug_log("🎊 所有測試完成!準備使用 MCP Feedback Enhanced")
debug_log("\n📖 使用方法:")
debug_log(" 1. 在 Cursor/Cline 中配置此 MCP 服務器")
debug_log(" 2. AI 助手會自動調用 interactive_feedback 工具")
debug_log(" 3. 根據環境自動選擇 GUI 或 Web UI")
debug_log(" 4. 提供回饋後繼續工作流程")
debug_log("\n✨ Web UI 新功能:")
debug_log(" - 支援 SSH remote 開發環境")
debug_log(" - 現代化深色主題界面")
debug_log(" - WebSocket 即時通訊")
debug_log(" - 自動瀏覽器啟動")
debug_log(" - 命令執行和即時輸出")
debug_log("\n✅ 測試完成 - 系統已準備就緒!")
if session_info:
debug_log(f"💡 您可以現在就在瀏覽器中測試: {session_info['url']}")
debug_log(" (服務器會繼續運行一小段時間)")
time.sleep(10) # Keep running for a short time for immediate testing
else:
debug_log("❌ 部分測試失敗,請檢查錯誤信息")
sys.exit(1)

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP 測試框架
============
完整的 MCP 測試系統,模擬真實的 Cursor IDE 調用場景。
主要功能:
- MCP 客戶端模擬器
- 完整的回饋循環測試
- 多場景測試覆蓋
- 詳細的測試報告
作者: Augment Agent
創建時間: 2025-01-05
"""
from .mcp_client import MCPTestClient
from .scenarios import TestScenarios
from .validators import TestValidators
from .reporter import TestReporter
from .utils import TestUtils
from .config import TestConfig, DEFAULT_CONFIG
__all__ = [
'MCPTestClient',
'TestScenarios',
'TestValidators',
'TestReporter',
'TestUtils',
'TestConfig',
'DEFAULT_CONFIG'
]
__version__ = "1.0.0"
__author__ = "Augment Agent"

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
測試配置管理
============
管理 MCP 測試框架的配置參數和設定。
"""
import os
from dataclasses import dataclass
from typing import Dict, Any, Optional
from pathlib import Path
@dataclass
class TestConfig:
"""測試配置類"""
# 服務器配置
server_host: str = "127.0.0.1"
server_port: int = 8765
server_timeout: int = 30
# MCP 客戶端配置
mcp_timeout: int = 60
mcp_retry_count: int = 3
mcp_retry_delay: float = 1.0
# WebSocket 配置
websocket_timeout: int = 10
websocket_ping_interval: int = 5
websocket_ping_timeout: int = 3
# 測試配置
test_timeout: int = 120
test_parallel: bool = False
test_verbose: bool = True
test_debug: bool = False
# 報告配置
report_format: str = "html" # html, json, markdown
report_output_dir: str = "test_reports"
report_include_logs: bool = True
report_include_performance: bool = True
# 測試數據配置
test_project_dir: Optional[str] = None
test_summary: str = "MCP 測試框架 - 模擬 Cursor IDE 調用"
test_feedback_text: str = "這是一個測試回饋,用於驗證 MCP 系統功能。"
@classmethod
def from_env(cls) -> 'TestConfig':
"""從環境變數創建配置"""
config = cls()
# 從環境變數讀取配置
config.server_host = os.getenv('MCP_TEST_HOST', config.server_host)
config.server_port = int(os.getenv('MCP_TEST_PORT', str(config.server_port)))
config.server_timeout = int(os.getenv('MCP_TEST_SERVER_TIMEOUT', str(config.server_timeout)))
config.mcp_timeout = int(os.getenv('MCP_TEST_TIMEOUT', str(config.mcp_timeout)))
config.mcp_retry_count = int(os.getenv('MCP_TEST_RETRY_COUNT', str(config.mcp_retry_count)))
config.test_timeout = int(os.getenv('MCP_TEST_CASE_TIMEOUT', str(config.test_timeout)))
config.test_parallel = os.getenv('MCP_TEST_PARALLEL', '').lower() in ('true', '1', 'yes')
config.test_verbose = os.getenv('MCP_TEST_VERBOSE', '').lower() not in ('false', '0', 'no')
config.test_debug = os.getenv('MCP_DEBUG', '').lower() in ('true', '1', 'yes')
config.report_format = os.getenv('MCP_TEST_REPORT_FORMAT', config.report_format)
config.report_output_dir = os.getenv('MCP_TEST_REPORT_DIR', config.report_output_dir)
config.test_project_dir = os.getenv('MCP_TEST_PROJECT_DIR', config.test_project_dir)
return config
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'TestConfig':
"""從字典創建配置"""
config = cls()
for key, value in data.items():
if hasattr(config, key):
setattr(config, key, value)
return config
def to_dict(self) -> Dict[str, Any]:
"""轉換為字典"""
return {
'server_host': self.server_host,
'server_port': self.server_port,
'server_timeout': self.server_timeout,
'mcp_timeout': self.mcp_timeout,
'mcp_retry_count': self.mcp_retry_count,
'mcp_retry_delay': self.mcp_retry_delay,
'websocket_timeout': self.websocket_timeout,
'websocket_ping_interval': self.websocket_ping_interval,
'websocket_ping_timeout': self.websocket_ping_timeout,
'test_timeout': self.test_timeout,
'test_parallel': self.test_parallel,
'test_verbose': self.test_verbose,
'test_debug': self.test_debug,
'report_format': self.report_format,
'report_output_dir': self.report_output_dir,
'report_include_logs': self.report_include_logs,
'report_include_performance': self.report_include_performance,
'test_project_dir': self.test_project_dir,
'test_summary': self.test_summary,
'test_feedback_text': self.test_feedback_text
}
def get_server_url(self) -> str:
"""獲取服務器 URL"""
return f"http://{self.server_host}:{self.server_port}"
def get_websocket_url(self) -> str:
"""獲取 WebSocket URL"""
return f"ws://{self.server_host}:{self.server_port}/ws"
def get_report_output_path(self) -> Path:
"""獲取報告輸出路徑"""
return Path(self.report_output_dir)
def ensure_report_dir(self) -> Path:
"""確保報告目錄存在"""
report_dir = self.get_report_output_path()
report_dir.mkdir(parents=True, exist_ok=True)
return report_dir
# 默認配置實例
DEFAULT_CONFIG = TestConfig.from_env()

View File

@@ -0,0 +1,527 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP 客戶端模擬器
================
模擬 Cursor IDE 作為 MCP 客戶端的完整調用流程,實現標準的 JSON-RPC 2.0 通信協議。
主要功能:
- MCP 協議握手和初始化
- 工具發現和能力協商
- 工具調用和結果處理
- 錯誤處理和重連機制
"""
import asyncio
import json
import uuid
import time
import subprocess
import signal
import os
from typing import Dict, Any, Optional, List, Callable, Awaitable
from pathlib import Path
from dataclasses import dataclass, field
from .config import TestConfig, DEFAULT_CONFIG
from .utils import TestUtils, PerformanceMonitor, AsyncEventWaiter
from ..debug import debug_log
@dataclass
class MCPMessage:
"""MCP 消息類"""
jsonrpc: str = "2.0"
id: Optional[str] = None
method: Optional[str] = None
params: Optional[Dict[str, Any]] = None
result: Optional[Any] = None
error: Optional[Dict[str, Any]] = None
def to_dict(self) -> Dict[str, Any]:
"""轉換為字典"""
data = {"jsonrpc": self.jsonrpc}
if self.id is not None:
data["id"] = self.id
if self.method is not None:
data["method"] = self.method
if self.params is not None:
data["params"] = self.params
if self.result is not None:
data["result"] = self.result
if self.error is not None:
data["error"] = self.error
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'MCPMessage':
"""從字典創建"""
return cls(
jsonrpc=data.get("jsonrpc", "2.0"),
id=data.get("id"),
method=data.get("method"),
params=data.get("params"),
result=data.get("result"),
error=data.get("error")
)
def is_request(self) -> bool:
"""是否為請求消息"""
return self.method is not None
def is_response(self) -> bool:
"""是否為響應消息"""
return self.result is not None or self.error is not None
def is_notification(self) -> bool:
"""是否為通知消息"""
return self.method is not None and self.id is None
@dataclass
class MCPClientState:
"""MCP 客戶端狀態"""
connected: bool = False
initialized: bool = False
tools_discovered: bool = False
available_tools: List[Dict[str, Any]] = field(default_factory=list)
server_capabilities: Dict[str, Any] = field(default_factory=dict)
client_info: Dict[str, Any] = field(default_factory=dict)
server_info: Dict[str, Any] = field(default_factory=dict)
class MCPTestClient:
"""MCP 測試客戶端"""
def __init__(self, config: Optional[TestConfig] = None):
self.config = config or DEFAULT_CONFIG
self.state = MCPClientState()
self.process: Optional[subprocess.Popen] = None
self.event_waiter = AsyncEventWaiter()
self.performance_monitor = PerformanceMonitor()
self.message_id_counter = 0
self.pending_requests: Dict[str, asyncio.Future] = {}
self.message_handlers: Dict[str, Callable] = {}
# 設置默認消息處理器
self._setup_default_handlers()
def _setup_default_handlers(self):
"""設置默認消息處理器"""
self.message_handlers.update({
'initialize': self._handle_initialize_response,
'tools/list': self._handle_tools_list_response,
'tools/call': self._handle_tools_call_response,
})
def _generate_message_id(self) -> str:
"""生成消息 ID"""
self.message_id_counter += 1
return f"msg_{self.message_id_counter}_{uuid.uuid4().hex[:8]}"
async def start_server(self) -> bool:
"""啟動 MCP 服務器"""
try:
debug_log("🚀 啟動 MCP 服務器...")
self.performance_monitor.start()
# 構建啟動命令
cmd = [
"python", "-m", "src.mcp_feedback_enhanced", "server"
]
# 設置環境變數
env = os.environ.copy()
env.update({
"MCP_DEBUG": "true" if self.config.test_debug else "false",
"PYTHONPATH": str(Path(__file__).parent.parent.parent.parent)
})
# 啟動進程
self.process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
bufsize=0
)
debug_log(f"✅ MCP 服務器進程已啟動 (PID: {self.process.pid})")
# 等待服務器初始化
await asyncio.sleep(2)
# 檢查進程是否仍在運行
if self.process.poll() is not None:
stderr_output = self.process.stderr.read() if self.process.stderr else ""
raise RuntimeError(f"MCP 服務器啟動失敗: {stderr_output}")
self.state.connected = True
self.performance_monitor.checkpoint("server_started")
return True
except Exception as e:
debug_log(f"❌ 啟動 MCP 服務器失敗: {e}")
await self.cleanup()
return False
async def stop_server(self):
"""停止 MCP 服務器"""
if self.process:
try:
debug_log("🛑 停止 MCP 服務器...")
# 嘗試優雅關閉
self.process.terminate()
try:
await asyncio.wait_for(
asyncio.create_task(self._wait_for_process()),
timeout=5.0
)
except asyncio.TimeoutError:
debug_log("⚠️ 優雅關閉超時,強制終止進程")
self.process.kill()
await self._wait_for_process()
debug_log("✅ MCP 服務器已停止")
except Exception as e:
debug_log(f"⚠️ 停止 MCP 服務器時發生錯誤: {e}")
finally:
self.process = None
self.state.connected = False
async def _wait_for_process(self):
"""等待進程結束"""
if self.process:
while self.process.poll() is None:
await asyncio.sleep(0.1)
async def send_message(self, message: MCPMessage) -> Optional[MCPMessage]:
"""發送 MCP 消息"""
if not self.process or not self.state.connected:
raise RuntimeError("MCP 服務器未連接")
try:
# 序列化消息
message_data = json.dumps(message.to_dict()) + "\n"
debug_log(f"📤 發送 MCP 消息: {message.method or 'response'}")
if self.config.test_debug:
debug_log(f" 內容: {message_data.strip()}")
# 發送消息
self.process.stdin.write(message_data)
self.process.stdin.flush()
# 如果是請求,等待響應
if message.is_request() and message.id:
future = asyncio.Future()
self.pending_requests[message.id] = future
try:
response = await asyncio.wait_for(
future,
timeout=self.config.mcp_timeout
)
return response
except asyncio.TimeoutError:
self.pending_requests.pop(message.id, None)
raise TimeoutError(f"MCP 請求超時: {message.method}")
return None
except Exception as e:
debug_log(f"❌ 發送 MCP 消息失敗: {e}")
raise
async def read_messages(self):
"""讀取 MCP 消息"""
if not self.process:
return
try:
while self.process and self.process.poll() is None:
# 讀取一行
line = await asyncio.create_task(self._read_line())
if not line:
continue
try:
# 解析 JSON
data = json.loads(line.strip())
message = MCPMessage.from_dict(data)
debug_log(f"📨 收到 MCP 消息: {message.method or 'response'}")
if self.config.test_debug:
debug_log(f" 內容: {line.strip()}")
# 處理消息
await self._handle_message(message)
except json.JSONDecodeError as e:
debug_log(f"⚠️ JSON 解析失敗: {e}, 原始數據: {line}")
except Exception as e:
debug_log(f"❌ 處理消息失敗: {e}")
except Exception as e:
debug_log(f"❌ 讀取 MCP 消息失敗: {e}")
async def _read_line(self) -> str:
"""異步讀取一行"""
if not self.process or not self.process.stdout:
return ""
# 使用線程池執行阻塞的讀取操作
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, self.process.stdout.readline)
async def _handle_message(self, message: MCPMessage):
"""處理收到的消息"""
if message.is_response() and message.id:
# 處理響應
future = self.pending_requests.pop(message.id, None)
if future and not future.done():
future.set_result(message)
elif message.is_request():
# 處理請求(通常是服務器發起的)
debug_log(f"收到服務器請求: {message.method}")
# 調用特定的消息處理器
if message.method in self.message_handlers:
await self.message_handlers[message.method](message)
async def _handle_initialize_response(self, message: MCPMessage):
"""處理初始化響應"""
if message.result:
self.state.server_info = message.result.get('serverInfo', {})
self.state.server_capabilities = message.result.get('capabilities', {})
self.state.initialized = True
debug_log("✅ MCP 初始化完成")
async def _handle_tools_list_response(self, message: MCPMessage):
"""處理工具列表響應"""
if message.result and 'tools' in message.result:
self.state.available_tools = message.result['tools']
self.state.tools_discovered = True
debug_log(f"✅ 發現 {len(self.state.available_tools)} 個工具")
async def _handle_tools_call_response(self, message: MCPMessage):
"""處理工具調用響應"""
if message.result:
debug_log("✅ 工具調用完成")
elif message.error:
debug_log(f"❌ 工具調用失敗: {message.error}")
async def initialize(self) -> bool:
"""初始化 MCP 連接"""
try:
debug_log("🔄 初始化 MCP 連接...")
message = MCPMessage(
id=self._generate_message_id(),
method="initialize",
params={
"protocolVersion": "2024-11-05",
"clientInfo": {
"name": "mcp-test-client",
"version": "1.0.0"
},
"capabilities": {
"roots": {
"listChanged": True
},
"sampling": {}
}
}
)
response = await self.send_message(message)
if response and response.result:
self.performance_monitor.checkpoint("initialized")
return True
else:
debug_log(f"❌ 初始化失敗: {response.error if response else '無響應'}")
return False
except Exception as e:
debug_log(f"❌ 初始化異常: {e}")
return False
async def list_tools(self) -> List[Dict[str, Any]]:
"""獲取可用工具列表"""
try:
debug_log("🔍 獲取工具列表...")
message = MCPMessage(
id=self._generate_message_id(),
method="tools/list",
params={}
)
response = await self.send_message(message)
if response and response.result and 'tools' in response.result:
tools = response.result['tools']
debug_log(f"✅ 獲取到 {len(tools)} 個工具")
self.performance_monitor.checkpoint("tools_listed", {"tools_count": len(tools)})
return tools
else:
debug_log(f"❌ 獲取工具列表失敗: {response.error if response else '無響應'}")
return []
except Exception as e:
debug_log(f"❌ 獲取工具列表異常: {e}")
return []
async def call_interactive_feedback(self, project_directory: str, summary: str,
timeout: int = 60) -> Dict[str, Any]:
"""調用互動回饋工具"""
try:
debug_log("🎯 調用互動回饋工具...")
message = MCPMessage(
id=self._generate_message_id(),
method="tools/call",
params={
"name": "interactive_feedback",
"arguments": {
"project_directory": project_directory,
"summary": summary,
"timeout": timeout
}
}
)
# 設置較長的超時時間,因為需要等待用戶互動
old_timeout = self.config.mcp_timeout
self.config.mcp_timeout = timeout + 30 # 額外 30 秒緩衝
try:
response = await self.send_message(message)
if response and response.result:
result = response.result
debug_log("✅ 互動回饋工具調用成功")
self.performance_monitor.checkpoint("interactive_feedback_completed")
return result
else:
error_msg = response.error if response else "無響應"
debug_log(f"❌ 互動回饋工具調用失敗: {error_msg}")
return {"error": str(error_msg)}
finally:
self.config.mcp_timeout = old_timeout
except Exception as e:
debug_log(f"❌ 互動回饋工具調用異常: {e}")
return {"error": str(e)}
async def full_workflow_test(self, project_directory: Optional[str] = None,
summary: Optional[str] = None) -> Dict[str, Any]:
"""執行完整的工作流程測試"""
try:
debug_log("🚀 開始完整工作流程測試...")
self.performance_monitor.start()
# 使用配置中的默認值
project_dir = project_directory or self.config.test_project_dir or str(Path.cwd())
test_summary = summary or self.config.test_summary
results = {
"success": False,
"steps": {},
"performance": {},
"errors": []
}
# 步驟 1: 啟動服務器
if not await self.start_server():
results["errors"].append("服務器啟動失敗")
return results
results["steps"]["server_started"] = True
# 啟動消息讀取任務
read_task = asyncio.create_task(self.read_messages())
try:
# 步驟 2: 初始化連接
if not await self.initialize():
results["errors"].append("MCP 初始化失敗")
return results
results["steps"]["initialized"] = True
# 步驟 3: 獲取工具列表
tools = await self.list_tools()
if not tools:
results["errors"].append("獲取工具列表失敗")
return results
results["steps"]["tools_discovered"] = True
results["tools_count"] = len(tools)
# 檢查是否有 interactive_feedback 工具
has_interactive_tool = any(
tool.get("name") == "interactive_feedback"
for tool in tools
)
if not has_interactive_tool:
results["errors"].append("未找到 interactive_feedback 工具")
return results
# 步驟 4: 調用互動回饋工具
feedback_result = await self.call_interactive_feedback(
project_dir, test_summary, self.config.test_timeout
)
if "error" in feedback_result:
results["errors"].append(f"互動回饋調用失敗: {feedback_result['error']}")
return results
results["steps"]["interactive_feedback_called"] = True
results["feedback_result"] = feedback_result
results["success"] = True
debug_log("🎉 完整工作流程測試成功完成")
finally:
read_task.cancel()
try:
await read_task
except asyncio.CancelledError:
pass
return results
except Exception as e:
debug_log(f"❌ 完整工作流程測試異常: {e}")
results["errors"].append(f"測試異常: {str(e)}")
return results
finally:
# 獲取性能數據
self.performance_monitor.stop()
results["performance"] = self.performance_monitor.get_summary()
# 清理資源
await self.cleanup()
async def cleanup(self):
"""清理資源"""
await self.stop_server()
# 取消所有待處理的請求
for future in self.pending_requests.values():
if not future.done():
future.cancel()
self.pending_requests.clear()
self.performance_monitor.stop()
debug_log("🧹 MCP 客戶端資源已清理")

View File

@@ -0,0 +1,447 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
測試報告生成器
==============
生成詳細的 MCP 測試報告,支持多種格式輸出。
"""
import json
import time
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
from pathlib import Path
from dataclasses import dataclass, asdict
from .config import TestConfig, DEFAULT_CONFIG
from .utils import TestUtils
from .validators import TestValidators, ValidationResult
from ..debug import debug_log
@dataclass
class TestReport:
"""測試報告數據結構"""
timestamp: str
duration: float
total_scenarios: int
passed_scenarios: int
failed_scenarios: int
success_rate: float
scenarios: List[Dict[str, Any]]
validation_summary: Dict[str, Any]
performance_summary: Dict[str, Any]
system_info: Dict[str, Any]
config: Dict[str, Any]
errors: List[str]
warnings: List[str]
class TestReporter:
"""測試報告生成器"""
def __init__(self, config: Optional[TestConfig] = None):
self.config = config or DEFAULT_CONFIG
self.validators = TestValidators(config)
def generate_report(self, test_results: Dict[str, Any]) -> TestReport:
"""生成測試報告"""
start_time = time.time()
# 提取基本信息
scenarios = test_results.get("results", [])
total_scenarios = test_results.get("total_scenarios", len(scenarios))
passed_scenarios = test_results.get("passed_scenarios", 0)
failed_scenarios = test_results.get("failed_scenarios", 0)
# 計算成功率
success_rate = passed_scenarios / total_scenarios if total_scenarios > 0 else 0
# 驗證測試結果
validation_results = {}
for i, scenario in enumerate(scenarios):
validation_results[f"scenario_{i}"] = self.validators.result_validator.validate_test_result(scenario)
validation_summary = self.validators.get_validation_summary(validation_results)
# 生成性能摘要
performance_summary = self._generate_performance_summary(scenarios)
# 收集錯誤和警告
all_errors = []
all_warnings = []
for scenario in scenarios:
all_errors.extend(scenario.get("errors", []))
# 計算總持續時間
total_duration = 0
for scenario in scenarios:
perf = scenario.get("performance", {})
duration = perf.get("total_duration", 0) or perf.get("total_time", 0)
total_duration += duration
# 創建報告
report = TestReport(
timestamp=datetime.now().isoformat(),
duration=total_duration,
total_scenarios=total_scenarios,
passed_scenarios=passed_scenarios,
failed_scenarios=failed_scenarios,
success_rate=success_rate,
scenarios=scenarios,
validation_summary=validation_summary,
performance_summary=performance_summary,
system_info=TestUtils.get_system_info(),
config=self.config.to_dict(),
errors=all_errors,
warnings=all_warnings
)
debug_log(f"📊 測試報告生成完成 (耗時: {time.time() - start_time:.2f}s)")
return report
def _generate_performance_summary(self, scenarios: List[Dict[str, Any]]) -> Dict[str, Any]:
"""生成性能摘要"""
total_duration = 0
min_duration = float('inf')
max_duration = 0
durations = []
memory_usage = []
for scenario in scenarios:
perf = scenario.get("performance", {})
# 處理持續時間
duration = perf.get("total_duration", 0) or perf.get("total_time", 0)
if duration > 0:
total_duration += duration
min_duration = min(min_duration, duration)
max_duration = max(max_duration, duration)
durations.append(duration)
# 處理內存使用
memory_diff = perf.get("memory_diff", {})
if memory_diff:
memory_usage.append(memory_diff)
# 計算平均值
avg_duration = total_duration / len(durations) if durations else 0
# 計算中位數
if durations:
sorted_durations = sorted(durations)
n = len(sorted_durations)
median_duration = (
sorted_durations[n // 2] if n % 2 == 1
else (sorted_durations[n // 2 - 1] + sorted_durations[n // 2]) / 2
)
else:
median_duration = 0
return {
"total_duration": total_duration,
"total_duration_formatted": TestUtils.format_duration(total_duration),
"avg_duration": avg_duration,
"avg_duration_formatted": TestUtils.format_duration(avg_duration),
"median_duration": median_duration,
"median_duration_formatted": TestUtils.format_duration(median_duration),
"min_duration": min_duration if min_duration != float('inf') else 0,
"min_duration_formatted": TestUtils.format_duration(min_duration if min_duration != float('inf') else 0),
"max_duration": max_duration,
"max_duration_formatted": TestUtils.format_duration(max_duration),
"scenarios_with_performance": len(durations),
"memory_usage_samples": len(memory_usage)
}
def save_report(self, report: TestReport, output_path: Optional[Path] = None) -> Path:
"""保存測試報告"""
if output_path is None:
output_dir = self.config.ensure_report_dir()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"mcp_test_report_{timestamp}.{self.config.report_format}"
output_path = output_dir / filename
output_path.parent.mkdir(parents=True, exist_ok=True)
if self.config.report_format.lower() == "json":
self._save_json_report(report, output_path)
elif self.config.report_format.lower() == "html":
self._save_html_report(report, output_path)
elif self.config.report_format.lower() == "markdown":
self._save_markdown_report(report, output_path)
else:
raise ValueError(f"不支持的報告格式: {self.config.report_format}")
debug_log(f"📄 測試報告已保存: {output_path}")
return output_path
def _save_json_report(self, report: TestReport, output_path: Path):
"""保存 JSON 格式報告"""
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(asdict(report), f, indent=2, ensure_ascii=False, default=str)
def _save_html_report(self, report: TestReport, output_path: Path):
"""保存 HTML 格式報告"""
html_content = self._generate_html_report(report)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
def _save_markdown_report(self, report: TestReport, output_path: Path):
"""保存 Markdown 格式報告"""
markdown_content = self._generate_markdown_report(report)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(markdown_content)
def _generate_html_report(self, report: TestReport) -> str:
"""生成 HTML 報告"""
# 狀態圖標
status_icon = "" if report.success_rate == 1.0 else "" if report.success_rate == 0 else "⚠️"
# 性能圖表數據(簡化版)
scenario_names = [s.get("scenario_name", f"Scenario {i}") for i, s in enumerate(report.scenarios)]
scenario_durations = []
for s in report.scenarios:
perf = s.get("performance", {})
duration = perf.get("total_duration", 0) or perf.get("total_time", 0)
scenario_durations.append(duration)
html = f"""
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP 測試報告</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
.container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
.header {{ text-align: center; margin-bottom: 30px; }}
.status {{ font-size: 24px; margin: 10px 0; }}
.success {{ color: #28a745; }}
.warning {{ color: #ffc107; }}
.error {{ color: #dc3545; }}
.summary {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; }}
.card {{ background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #007bff; }}
.card h3 {{ margin: 0 0 10px 0; color: #333; }}
.card .value {{ font-size: 24px; font-weight: bold; color: #007bff; }}
.scenarios {{ margin: 20px 0; }}
.scenario {{ background: #f8f9fa; margin: 10px 0; padding: 15px; border-radius: 6px; border-left: 4px solid #28a745; }}
.scenario.failed {{ border-left-color: #dc3545; }}
.scenario h4 {{ margin: 0 0 10px 0; }}
.scenario-details {{ display: grid; grid-template-columns: 1fr 1fr; gap: 10px; font-size: 14px; }}
.errors {{ background: #f8d7da; color: #721c24; padding: 10px; border-radius: 4px; margin: 10px 0; }}
.performance {{ margin: 20px 0; }}
.footer {{ text-align: center; margin-top: 30px; color: #666; font-size: 12px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 MCP 測試報告</h1>
<div class="status {'success' if report.success_rate == 1.0 else 'warning' if report.success_rate > 0 else 'error'}">
{status_icon} 測試完成
</div>
<p>生成時間: {report.timestamp}</p>
</div>
<div class="summary">
<div class="card">
<h3>總測試數</h3>
<div class="value">{report.total_scenarios}</div>
</div>
<div class="card">
<h3>通過測試</h3>
<div class="value" style="color: #28a745;">{report.passed_scenarios}</div>
</div>
<div class="card">
<h3>失敗測試</h3>
<div class="value" style="color: #dc3545;">{report.failed_scenarios}</div>
</div>
<div class="card">
<h3>成功率</h3>
<div class="value">{report.success_rate:.1%}</div>
</div>
<div class="card">
<h3>總耗時</h3>
<div class="value">{report.performance_summary.get('total_duration_formatted', 'N/A')}</div>
</div>
<div class="card">
<h3>平均耗時</h3>
<div class="value">{report.performance_summary.get('avg_duration_formatted', 'N/A')}</div>
</div>
</div>
<div class="scenarios">
<h2>📋 測試場景詳情</h2>
"""
for i, scenario in enumerate(report.scenarios):
success = scenario.get("success", False)
scenario_name = scenario.get("scenario_name", f"Scenario {i+1}")
scenario_desc = scenario.get("scenario_description", "無描述")
perf = scenario.get("performance", {})
duration = perf.get("total_duration", 0) or perf.get("total_time", 0)
duration_str = TestUtils.format_duration(duration) if duration > 0 else "N/A"
steps = scenario.get("steps", {})
completed_steps = sum(1 for v in steps.values() if v)
total_steps = len(steps)
errors = scenario.get("errors", [])
html += f"""
<div class="scenario {'failed' if not success else ''}">
<h4>{'' if success else ''} {scenario_name}</h4>
<p>{scenario_desc}</p>
<div class="scenario-details">
<div><strong>狀態:</strong> {'通過' if success else '失敗'}</div>
<div><strong>耗時:</strong> {duration_str}</div>
<div><strong>完成步驟:</strong> {completed_steps}/{total_steps}</div>
<div><strong>錯誤數:</strong> {len(errors)}</div>
</div>
"""
if errors:
html += '<div class="errors"><strong>錯誤信息:</strong><ul>'
for error in errors:
html += f'<li>{error}</li>'
html += '</ul></div>'
html += '</div>'
html += f"""
</div>
<div class="performance">
<h2>📊 性能統計</h2>
<div class="summary">
<div class="card">
<h3>最快測試</h3>
<div class="value">{report.performance_summary.get('min_duration_formatted', 'N/A')}</div>
</div>
<div class="card">
<h3>最慢測試</h3>
<div class="value">{report.performance_summary.get('max_duration_formatted', 'N/A')}</div>
</div>
<div class="card">
<h3>中位數</h3>
<div class="value">{report.performance_summary.get('median_duration_formatted', 'N/A')}</div>
</div>
</div>
</div>
<div class="footer">
<p>MCP Feedback Enhanced 測試框架 | 生成時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
</div>
</body>
</html>
"""
return html
def _generate_markdown_report(self, report: TestReport) -> str:
"""生成 Markdown 報告"""
status_icon = "" if report.success_rate == 1.0 else "" if report.success_rate == 0 else "⚠️"
md = f"""# 🧪 MCP 測試報告
{status_icon} **測試狀態**: {'全部通過' if report.success_rate == 1.0 else '部分失敗' if report.success_rate > 0 else '全部失敗'}
**生成時間**: {report.timestamp}
## 📊 測試摘要
| 指標 | 數值 |
|------|------|
| 總測試數 | {report.total_scenarios} |
| 通過測試 | {report.passed_scenarios} |
| 失敗測試 | {report.failed_scenarios} |
| 成功率 | {report.success_rate:.1%} |
| 總耗時 | {report.performance_summary.get('total_duration_formatted', 'N/A')} |
| 平均耗時 | {report.performance_summary.get('avg_duration_formatted', 'N/A')} |
## 📋 測試場景詳情
"""
for i, scenario in enumerate(report.scenarios):
success = scenario.get("success", False)
scenario_name = scenario.get("scenario_name", f"Scenario {i+1}")
scenario_desc = scenario.get("scenario_description", "無描述")
perf = scenario.get("performance", {})
duration = perf.get("total_duration", 0) or perf.get("total_time", 0)
duration_str = TestUtils.format_duration(duration) if duration > 0 else "N/A"
steps = scenario.get("steps", {})
completed_steps = sum(1 for v in steps.values() if v)
total_steps = len(steps)
errors = scenario.get("errors", [])
md += f"""### {'' if success else ''} {scenario_name}
**描述**: {scenario_desc}
- **狀態**: {'通過' if success else '失敗'}
- **耗時**: {duration_str}
- **完成步驟**: {completed_steps}/{total_steps}
- **錯誤數**: {len(errors)}
"""
if errors:
md += "**錯誤信息**:\n"
for error in errors:
md += f"- {error}\n"
md += "\n"
md += f"""## 📊 性能統計
| 指標 | 數值 |
|------|------|
| 最快測試 | {report.performance_summary.get('min_duration_formatted', 'N/A')} |
| 最慢測試 | {report.performance_summary.get('max_duration_formatted', 'N/A')} |
| 中位數 | {report.performance_summary.get('median_duration_formatted', 'N/A')} |
## 🔧 系統信息
| 項目 | 值 |
|------|---|
| CPU 核心數 | {report.system_info.get('cpu_count', 'N/A')} |
| 總內存 | {report.system_info.get('memory_total', 'N/A')} |
| 可用內存 | {report.system_info.get('memory_available', 'N/A')} |
---
*報告由 MCP Feedback Enhanced 測試框架生成 | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
"""
return md
def print_summary(self, report: TestReport):
"""打印測試摘要到控制台"""
status_icon = "" if report.success_rate == 1.0 else "" if report.success_rate == 0 else "⚠️"
print("\n" + "="*60)
print(f"🧪 MCP 測試報告摘要 {status_icon}")
print("="*60)
print(f"📊 總測試數: {report.total_scenarios}")
print(f"✅ 通過測試: {report.passed_scenarios}")
print(f"❌ 失敗測試: {report.failed_scenarios}")
print(f"📈 成功率: {report.success_rate:.1%}")
print(f"⏱️ 總耗時: {report.performance_summary.get('total_duration_formatted', 'N/A')}")
print(f"⚡ 平均耗時: {report.performance_summary.get('avg_duration_formatted', 'N/A')}")
if report.errors:
print(f"\n❌ 發現 {len(report.errors)} 個錯誤:")
for error in report.errors[:5]: # 只顯示前5個錯誤
print(f"{error}")
if len(report.errors) > 5:
print(f" ... 還有 {len(report.errors) - 5} 個錯誤")
print("="*60)

View File

@@ -0,0 +1,469 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
測試場景定義
============
定義各種 MCP 測試場景,包括正常流程、錯誤處理、性能測試等。
"""
import asyncio
import time
import random
from typing import Dict, Any, List, Optional, Callable, Awaitable
from dataclasses import dataclass, field
from pathlib import Path
from .mcp_client import MCPTestClient
from .config import TestConfig, DEFAULT_CONFIG
from .utils import TestUtils, PerformanceMonitor, performance_context
from ..debug import debug_log
@dataclass
class TestScenario:
"""測試場景類"""
name: str
description: str
timeout: int = 120
retry_count: int = 1
parallel: bool = False
tags: List[str] = field(default_factory=list)
setup: Optional[Callable] = None
teardown: Optional[Callable] = None
async def run(self, client: MCPTestClient) -> Dict[str, Any]:
"""運行測試場景"""
raise NotImplementedError
class BasicWorkflowScenario(TestScenario):
"""基礎工作流程測試場景"""
def __init__(self):
super().__init__(
name="basic_workflow",
description="測試基本的 MCP 工作流程:初始化 -> 工具發現 -> 工具調用",
timeout=180,
tags=["basic", "workflow", "integration"]
)
async def run(self, client: MCPTestClient) -> Dict[str, Any]:
"""運行基礎工作流程測試"""
async with performance_context("basic_workflow") as monitor:
result = await client.full_workflow_test()
# 添加額外的驗證
if result["success"]:
# 檢查必要的步驟是否完成
required_steps = ["server_started", "initialized", "tools_discovered", "interactive_feedback_called"]
missing_steps = [step for step in required_steps if not result["steps"].get(step, False)]
if missing_steps:
result["success"] = False
result["errors"].append(f"缺少必要步驟: {missing_steps}")
return result
class QuickConnectionScenario(TestScenario):
"""快速連接測試場景"""
def __init__(self):
super().__init__(
name="quick_connection",
description="測試 MCP 服務器的快速啟動和連接",
timeout=30,
tags=["quick", "connection", "startup"]
)
async def run(self, client: MCPTestClient) -> Dict[str, Any]:
"""運行快速連接測試"""
result = {
"success": False,
"steps": {},
"performance": {},
"errors": []
}
try:
start_time = time.time()
# 啟動服務器
if not await client.start_server():
result["errors"].append("服務器啟動失敗")
return result
result["steps"]["server_started"] = True
# 啟動消息讀取
read_task = asyncio.create_task(client.read_messages())
try:
# 初始化連接
if not await client.initialize():
result["errors"].append("初始化失敗")
return result
result["steps"]["initialized"] = True
# 獲取工具列表
tools = await client.list_tools()
if not tools:
result["errors"].append("工具列表為空")
return result
result["steps"]["tools_discovered"] = True
end_time = time.time()
result["performance"]["total_time"] = end_time - start_time
result["performance"]["tools_count"] = len(tools)
result["success"] = True
finally:
read_task.cancel()
try:
await read_task
except asyncio.CancelledError:
pass
except Exception as e:
result["errors"].append(f"測試異常: {str(e)}")
finally:
await client.cleanup()
return result
class TimeoutHandlingScenario(TestScenario):
"""超時處理測試場景"""
def __init__(self):
super().__init__(
name="timeout_handling",
description="測試超時情況下的處理機制",
timeout=60,
tags=["timeout", "error_handling", "resilience"]
)
async def run(self, client: MCPTestClient) -> Dict[str, Any]:
"""運行超時處理測試"""
result = {
"success": False,
"steps": {},
"performance": {},
"errors": []
}
try:
# 設置很短的超時時間來觸發超時
original_timeout = client.config.mcp_timeout
client.config.mcp_timeout = 5 # 5 秒超時
# 啟動服務器
if not await client.start_server():
result["errors"].append("服務器啟動失敗")
return result
result["steps"]["server_started"] = True
# 啟動消息讀取
read_task = asyncio.create_task(client.read_messages())
try:
# 初始化連接
if not await client.initialize():
result["errors"].append("初始化失敗")
return result
result["steps"]["initialized"] = True
# 嘗試調用互動回饋工具(應該超時)
feedback_result = await client.call_interactive_feedback(
str(Path.cwd()),
"超時測試 - 這個調用應該會超時",
timeout=10 # 10 秒超時,但 MCP 客戶端設置為 5 秒
)
# 檢查是否正確處理了超時
if "error" in feedback_result:
result["steps"]["timeout_handled"] = True
result["success"] = True
debug_log("✅ 超時處理測試成功")
else:
result["errors"].append("未正確處理超時情況")
finally:
read_task.cancel()
try:
await read_task
except asyncio.CancelledError:
pass
# 恢復原始超時設置
client.config.mcp_timeout = original_timeout
except Exception as e:
result["errors"].append(f"測試異常: {str(e)}")
finally:
await client.cleanup()
return result
class ConcurrentCallsScenario(TestScenario):
"""並發調用測試場景"""
def __init__(self):
super().__init__(
name="concurrent_calls",
description="測試並發 MCP 調用的處理能力",
timeout=300,
parallel=True,
tags=["concurrent", "performance", "stress"]
)
async def run(self, client: MCPTestClient) -> Dict[str, Any]:
"""運行並發調用測試"""
result = {
"success": False,
"steps": {},
"performance": {},
"errors": []
}
try:
# 啟動服務器
if not await client.start_server():
result["errors"].append("服務器啟動失敗")
return result
result["steps"]["server_started"] = True
# 啟動消息讀取
read_task = asyncio.create_task(client.read_messages())
try:
# 初始化連接
if not await client.initialize():
result["errors"].append("初始化失敗")
return result
result["steps"]["initialized"] = True
# 並發獲取工具列表
concurrent_count = 5
tasks = []
for i in range(concurrent_count):
task = asyncio.create_task(client.list_tools())
tasks.append(task)
start_time = time.time()
results = await asyncio.gather(*tasks, return_exceptions=True)
end_time = time.time()
# 分析結果
successful_calls = 0
failed_calls = 0
for i, res in enumerate(results):
if isinstance(res, Exception):
failed_calls += 1
debug_log(f"並發調用 {i+1} 失敗: {res}")
elif isinstance(res, list) and len(res) > 0:
successful_calls += 1
else:
failed_calls += 1
result["performance"]["concurrent_count"] = concurrent_count
result["performance"]["successful_calls"] = successful_calls
result["performance"]["failed_calls"] = failed_calls
result["performance"]["total_time"] = end_time - start_time
result["performance"]["avg_time_per_call"] = (end_time - start_time) / concurrent_count
# 判斷成功條件:至少 80% 的調用成功
success_rate = successful_calls / concurrent_count
if success_rate >= 0.8:
result["success"] = True
result["steps"]["concurrent_calls_handled"] = True
debug_log(f"✅ 並發調用測試成功 (成功率: {success_rate:.1%})")
else:
result["errors"].append(f"並發調用成功率過低: {success_rate:.1%}")
finally:
read_task.cancel()
try:
await read_task
except asyncio.CancelledError:
pass
except Exception as e:
result["errors"].append(f"測試異常: {str(e)}")
finally:
await client.cleanup()
return result
class MockTestScenario(TestScenario):
"""模擬測試場景(用於演示)"""
def __init__(self):
super().__init__(
name="mock_test",
description="模擬測試場景,用於演示測試框架功能",
timeout=10,
tags=["mock", "demo", "quick"]
)
async def run(self, client: MCPTestClient) -> Dict[str, Any]:
"""運行模擬測試"""
result = {
"success": True,
"steps": {
"mock_step_1": True,
"mock_step_2": True,
"mock_step_3": True
},
"performance": {
"total_duration": 0.5,
"total_time": 0.5
},
"errors": []
}
# 模擬一些處理時間
await asyncio.sleep(0.5)
debug_log("✅ 模擬測試完成")
return result
class TestScenarios:
"""測試場景管理器"""
def __init__(self, config: Optional[TestConfig] = None):
self.config = config or DEFAULT_CONFIG
self.scenarios: Dict[str, TestScenario] = {}
self._register_default_scenarios()
def _register_default_scenarios(self):
"""註冊默認測試場景"""
scenarios = [
MockTestScenario(), # 添加模擬測試場景
BasicWorkflowScenario(),
QuickConnectionScenario(),
TimeoutHandlingScenario(),
ConcurrentCallsScenario(),
]
for scenario in scenarios:
self.scenarios[scenario.name] = scenario
def register_scenario(self, scenario: TestScenario):
"""註冊自定義測試場景"""
self.scenarios[scenario.name] = scenario
def get_scenario(self, name: str) -> Optional[TestScenario]:
"""獲取測試場景"""
return self.scenarios.get(name)
def list_scenarios(self, tags: Optional[List[str]] = None) -> List[TestScenario]:
"""列出測試場景"""
scenarios = list(self.scenarios.values())
if tags:
scenarios = [
scenario for scenario in scenarios
if any(tag in scenario.tags for tag in tags)
]
return scenarios
async def run_scenario(self, scenario_name: str) -> Dict[str, Any]:
"""運行單個測試場景"""
scenario = self.get_scenario(scenario_name)
if not scenario:
return {
"success": False,
"errors": [f"未找到測試場景: {scenario_name}"]
}
debug_log(f"🧪 運行測試場景: {scenario.name}")
debug_log(f" 描述: {scenario.description}")
client = MCPTestClient(self.config)
try:
# 執行設置
if scenario.setup:
await scenario.setup()
# 運行測試
result = await TestUtils.timeout_wrapper(
scenario.run(client),
scenario.timeout,
f"測試場景 '{scenario.name}' 超時"
)
result["scenario_name"] = scenario.name
result["scenario_description"] = scenario.description
return result
except Exception as e:
debug_log(f"❌ 測試場景 '{scenario.name}' 執行失敗: {e}")
return {
"success": False,
"scenario_name": scenario.name,
"scenario_description": scenario.description,
"errors": [f"執行異常: {str(e)}"]
}
finally:
# 執行清理
if scenario.teardown:
try:
await scenario.teardown()
except Exception as e:
debug_log(f"⚠️ 測試場景 '{scenario.name}' 清理失敗: {e}")
async def run_all_scenarios(self, tags: Optional[List[str]] = None) -> Dict[str, Any]:
"""運行所有測試場景"""
scenarios = self.list_scenarios(tags)
if not scenarios:
return {
"success": False,
"total_scenarios": 0,
"passed_scenarios": 0,
"failed_scenarios": 0,
"results": [],
"errors": ["沒有找到匹配的測試場景"]
}
debug_log(f"🚀 開始運行 {len(scenarios)} 個測試場景...")
results = []
passed_count = 0
failed_count = 0
for scenario in scenarios:
result = await self.run_scenario(scenario.name)
results.append(result)
if result.get("success", False):
passed_count += 1
debug_log(f"{scenario.name}: 通過")
else:
failed_count += 1
debug_log(f"{scenario.name}: 失敗")
overall_success = failed_count == 0
debug_log(f"📊 測試完成: {passed_count}/{len(scenarios)} 通過")
return {
"success": overall_success,
"total_scenarios": len(scenarios),
"passed_scenarios": passed_count,
"failed_scenarios": failed_count,
"results": results
}

View File

@@ -0,0 +1,266 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
測試工具函數
============
提供 MCP 測試框架使用的通用工具函數。
"""
import asyncio
import time
import json
import uuid
import socket
import psutil
import threading
from typing import Dict, Any, Optional, List, Callable, Awaitable
from pathlib import Path
from datetime import datetime, timedelta
from contextlib import asynccontextmanager
from ..debug import debug_log
class TestUtils:
"""測試工具類"""
@staticmethod
def generate_test_id() -> str:
"""生成測試 ID"""
return f"test_{uuid.uuid4().hex[:8]}"
@staticmethod
def generate_session_id() -> str:
"""生成會話 ID"""
return str(uuid.uuid4())
@staticmethod
def get_timestamp() -> str:
"""獲取當前時間戳"""
return datetime.now().isoformat()
@staticmethod
def format_duration(seconds: float) -> str:
"""格式化持續時間"""
if seconds < 1:
return f"{seconds*1000:.1f}ms"
elif seconds < 60:
return f"{seconds:.2f}s"
else:
minutes = int(seconds // 60)
remaining_seconds = seconds % 60
return f"{minutes}m {remaining_seconds:.1f}s"
@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})")
@staticmethod
def is_port_open(host: str, port: int, timeout: float = 1.0) -> bool:
"""檢查端口是否開放"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
result = s.connect_ex((host, port))
return result == 0
except Exception:
return False
@staticmethod
async def wait_for_port(host: str, port: int, timeout: float = 30.0, interval: float = 0.5) -> bool:
"""等待端口開放"""
start_time = time.time()
while time.time() - start_time < timeout:
if TestUtils.is_port_open(host, port):
return True
await asyncio.sleep(interval)
return False
@staticmethod
def get_system_info() -> Dict[str, Any]:
"""獲取系統信息"""
try:
return {
'cpu_count': psutil.cpu_count(),
'memory_total': psutil.virtual_memory().total,
'memory_available': psutil.virtual_memory().available,
'disk_usage': psutil.disk_usage('/').percent if hasattr(psutil, 'disk_usage') else None,
'platform': psutil.WINDOWS if hasattr(psutil, 'WINDOWS') else 'unknown'
}
except Exception as e:
debug_log(f"獲取系統信息失敗: {e}")
return {}
@staticmethod
def measure_memory_usage() -> Dict[str, float]:
"""測量內存使用情況"""
try:
process = psutil.Process()
memory_info = process.memory_info()
return {
'rss': memory_info.rss / 1024 / 1024, # MB
'vms': memory_info.vms / 1024 / 1024, # MB
'percent': process.memory_percent()
}
except Exception as e:
debug_log(f"測量內存使用失敗: {e}")
return {}
@staticmethod
async def timeout_wrapper(coro: Awaitable, timeout: float, error_message: str = "操作超時"):
"""為協程添加超時包裝"""
try:
return await asyncio.wait_for(coro, timeout=timeout)
except asyncio.TimeoutError:
raise TimeoutError(f"{error_message} (超時: {timeout}s)")
@staticmethod
def safe_json_loads(data: str) -> Optional[Dict[str, Any]]:
"""安全的 JSON 解析"""
try:
return json.loads(data)
except (json.JSONDecodeError, TypeError) as e:
debug_log(f"JSON 解析失敗: {e}")
return None
@staticmethod
def safe_json_dumps(data: Any, indent: int = 2) -> str:
"""安全的 JSON 序列化"""
try:
return json.dumps(data, indent=indent, ensure_ascii=False, default=str)
except (TypeError, ValueError) as e:
debug_log(f"JSON 序列化失敗: {e}")
return str(data)
@staticmethod
def create_test_directory(base_dir: str = "test_temp") -> Path:
"""創建測試目錄"""
test_dir = Path(base_dir) / f"test_{uuid.uuid4().hex[:8]}"
test_dir.mkdir(parents=True, exist_ok=True)
return test_dir
@staticmethod
def cleanup_test_directory(test_dir: Path):
"""清理測試目錄"""
try:
if test_dir.exists() and test_dir.is_dir():
import shutil
shutil.rmtree(test_dir)
except Exception as e:
debug_log(f"清理測試目錄失敗: {e}")
class PerformanceMonitor:
"""性能監控器"""
def __init__(self):
self.start_time: Optional[float] = None
self.end_time: Optional[float] = None
self.memory_start: Optional[Dict[str, float]] = None
self.memory_end: Optional[Dict[str, float]] = None
self.checkpoints: List[Dict[str, Any]] = []
def start(self):
"""開始監控"""
self.start_time = time.time()
self.memory_start = TestUtils.measure_memory_usage()
self.checkpoints = []
def checkpoint(self, name: str, data: Optional[Dict[str, Any]] = None):
"""添加檢查點"""
if self.start_time is None:
return
checkpoint = {
'name': name,
'timestamp': time.time(),
'elapsed': time.time() - self.start_time,
'memory': TestUtils.measure_memory_usage(),
'data': data or {}
}
self.checkpoints.append(checkpoint)
def stop(self):
"""停止監控"""
self.end_time = time.time()
self.memory_end = TestUtils.measure_memory_usage()
def get_summary(self) -> Dict[str, Any]:
"""獲取監控摘要"""
if self.start_time is None or self.end_time is None:
return {}
total_duration = self.end_time - self.start_time
memory_diff = {}
if self.memory_start and self.memory_end:
for key in self.memory_start:
if key in self.memory_end:
memory_diff[f"memory_{key}_diff"] = self.memory_end[key] - self.memory_start[key]
return {
'total_duration': total_duration,
'total_duration_formatted': TestUtils.format_duration(total_duration),
'memory_start': self.memory_start,
'memory_end': self.memory_end,
'memory_diff': memory_diff,
'checkpoints_count': len(self.checkpoints),
'checkpoints': self.checkpoints
}
@asynccontextmanager
async def performance_context(name: str = "test"):
"""性能監控上下文管理器"""
monitor = PerformanceMonitor()
monitor.start()
try:
yield monitor
finally:
monitor.stop()
summary = monitor.get_summary()
debug_log(f"性能監控 [{name}]: {TestUtils.format_duration(summary.get('total_duration', 0))}")
class AsyncEventWaiter:
"""異步事件等待器"""
def __init__(self):
self.events: Dict[str, asyncio.Event] = {}
self.results: Dict[str, Any] = {}
def create_event(self, event_name: str):
"""創建事件"""
self.events[event_name] = asyncio.Event()
def set_event(self, event_name: str, result: Any = None):
"""設置事件"""
if event_name in self.events:
self.results[event_name] = result
self.events[event_name].set()
async def wait_for_event(self, event_name: str, timeout: float = 30.0) -> Any:
"""等待事件"""
if event_name not in self.events:
self.create_event(event_name)
try:
await asyncio.wait_for(self.events[event_name].wait(), timeout=timeout)
return self.results.get(event_name)
except asyncio.TimeoutError:
raise TimeoutError(f"等待事件 '{event_name}' 超時 ({timeout}s)")
def clear_event(self, event_name: str):
"""清除事件"""
if event_name in self.events:
self.events[event_name].clear()
self.results.pop(event_name, None)

View File

@@ -0,0 +1,394 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
測試結果驗證器
==============
驗證 MCP 測試結果是否符合規範和預期。
"""
import json
import re
from typing import Dict, Any, List, Optional, Union
from dataclasses import dataclass
from pathlib import Path
from .config import TestConfig, DEFAULT_CONFIG
from .utils import TestUtils
from ..debug import debug_log
@dataclass
class ValidationResult:
"""驗證結果"""
valid: bool
errors: List[str]
warnings: List[str]
details: Dict[str, Any]
def add_error(self, message: str):
"""添加錯誤"""
self.errors.append(message)
self.valid = False
def add_warning(self, message: str):
"""添加警告"""
self.warnings.append(message)
def add_detail(self, key: str, value: Any):
"""添加詳細信息"""
self.details[key] = value
class MCPMessageValidator:
"""MCP 消息驗證器"""
@staticmethod
def validate_json_rpc(message: Dict[str, Any]) -> ValidationResult:
"""驗證 JSON-RPC 2.0 格式"""
result = ValidationResult(True, [], [], {})
# 檢查必需字段
if "jsonrpc" not in message:
result.add_error("缺少 'jsonrpc' 字段")
elif message["jsonrpc"] != "2.0":
result.add_error(f"無效的 jsonrpc 版本: {message['jsonrpc']}")
# 檢查消息類型
is_request = "method" in message
is_response = "result" in message or "error" in message
is_notification = is_request and "id" not in message
if not (is_request or is_response):
result.add_error("消息既不是請求也不是響應")
if is_request and is_response:
result.add_error("消息不能同時是請求和響應")
# 驗證請求格式
if is_request:
if not isinstance(message.get("method"), str):
result.add_error("method 字段必須是字符串")
if not is_notification and "id" not in message:
result.add_error("非通知請求必須包含 id 字段")
# 驗證響應格式
if is_response:
if "id" not in message:
result.add_error("響應必須包含 id 字段")
if "result" in message and "error" in message:
result.add_error("響應不能同時包含 result 和 error")
if "result" not in message and "error" not in message:
result.add_error("響應必須包含 result 或 error")
# 驗證錯誤格式
if "error" in message:
error = message["error"]
if not isinstance(error, dict):
result.add_error("error 字段必須是對象")
else:
if "code" not in error:
result.add_error("error 對象必須包含 code 字段")
elif not isinstance(error["code"], int):
result.add_error("error.code 必須是整數")
if "message" not in error:
result.add_error("error 對象必須包含 message 字段")
elif not isinstance(error["message"], str):
result.add_error("error.message 必須是字符串")
result.add_detail("message_type", "request" if is_request else "response")
result.add_detail("is_notification", is_notification)
return result
@staticmethod
def validate_mcp_initialize(message: Dict[str, Any]) -> ValidationResult:
"""驗證 MCP 初始化消息"""
result = ValidationResult(True, [], [], {})
# 先驗證 JSON-RPC 格式
json_rpc_result = MCPMessageValidator.validate_json_rpc(message)
result.errors.extend(json_rpc_result.errors)
result.warnings.extend(json_rpc_result.warnings)
if not json_rpc_result.valid:
result.valid = False
return result
# 驗證初始化特定字段
if message.get("method") == "initialize":
params = message.get("params", {})
if "protocolVersion" not in params:
result.add_error("初始化請求必須包含 protocolVersion")
if "clientInfo" not in params:
result.add_error("初始化請求必須包含 clientInfo")
else:
client_info = params["clientInfo"]
if not isinstance(client_info, dict):
result.add_error("clientInfo 必須是對象")
else:
if "name" not in client_info:
result.add_error("clientInfo 必須包含 name")
if "version" not in client_info:
result.add_error("clientInfo 必須包含 version")
elif "result" in message:
# 驗證初始化響應
result_data = message.get("result", {})
if "serverInfo" not in result_data:
result.add_warning("初始化響應建議包含 serverInfo")
if "capabilities" not in result_data:
result.add_warning("初始化響應建議包含 capabilities")
return result
@staticmethod
def validate_tools_list(message: Dict[str, Any]) -> ValidationResult:
"""驗證工具列表消息"""
result = ValidationResult(True, [], [], {})
# 先驗證 JSON-RPC 格式
json_rpc_result = MCPMessageValidator.validate_json_rpc(message)
result.errors.extend(json_rpc_result.errors)
result.warnings.extend(json_rpc_result.warnings)
if not json_rpc_result.valid:
result.valid = False
return result
# 驗證工具列表響應
if "result" in message:
result_data = message.get("result", {})
if "tools" not in result_data:
result.add_error("工具列表響應必須包含 tools 字段")
else:
tools = result_data["tools"]
if not isinstance(tools, list):
result.add_error("tools 字段必須是數組")
else:
for i, tool in enumerate(tools):
if not isinstance(tool, dict):
result.add_error(f"工具 {i} 必須是對象")
continue
if "name" not in tool:
result.add_error(f"工具 {i} 必須包含 name 字段")
if "description" not in tool:
result.add_warning(f"工具 {i} 建議包含 description 字段")
if "inputSchema" not in tool:
result.add_warning(f"工具 {i} 建議包含 inputSchema 字段")
result.add_detail("tools_count", len(tools))
return result
class TestResultValidator:
"""測試結果驗證器"""
def __init__(self, config: Optional[TestConfig] = None):
self.config = config or DEFAULT_CONFIG
def validate_test_result(self, test_result: Dict[str, Any]) -> ValidationResult:
"""驗證測試結果"""
result = ValidationResult(True, [], [], {})
# 檢查必需字段
required_fields = ["success", "steps", "performance", "errors"]
for field in required_fields:
if field not in test_result:
result.add_error(f"測試結果缺少必需字段: {field}")
# 驗證成功標誌
if "success" in test_result:
if not isinstance(test_result["success"], bool):
result.add_error("success 字段必須是布爾值")
# 驗證步驟信息
if "steps" in test_result:
steps = test_result["steps"]
if not isinstance(steps, dict):
result.add_error("steps 字段必須是對象")
else:
result.add_detail("completed_steps", list(steps.keys()))
result.add_detail("steps_count", len(steps))
# 驗證錯誤信息
if "errors" in test_result:
errors = test_result["errors"]
if not isinstance(errors, list):
result.add_error("errors 字段必須是數組")
else:
result.add_detail("error_count", len(errors))
if len(errors) > 0 and test_result.get("success", False):
result.add_warning("測試標記為成功但包含錯誤信息")
# 驗證性能數據
if "performance" in test_result:
performance = test_result["performance"]
if not isinstance(performance, dict):
result.add_error("performance 字段必須是對象")
else:
self._validate_performance_data(performance, result)
return result
def _validate_performance_data(self, performance: Dict[str, Any], result: ValidationResult):
"""驗證性能數據"""
# 檢查時間相關字段
time_fields = ["total_duration", "total_time"]
for field in time_fields:
if field in performance:
value = performance[field]
if not isinstance(value, (int, float)):
result.add_error(f"性能字段 {field} 必須是數字")
elif value < 0:
result.add_error(f"性能字段 {field} 不能為負數")
elif value > self.config.test_timeout:
result.add_warning(f"性能字段 {field} 超過測試超時時間")
# 檢查內存相關字段
memory_fields = ["memory_start", "memory_end", "memory_diff"]
for field in memory_fields:
if field in performance:
value = performance[field]
if not isinstance(value, dict):
result.add_warning(f"性能字段 {field} 應該是對象")
# 檢查檢查點數據
if "checkpoints" in performance:
checkpoints = performance["checkpoints"]
if not isinstance(checkpoints, list):
result.add_error("checkpoints 字段必須是數組")
else:
result.add_detail("checkpoints_count", len(checkpoints))
def validate_interactive_feedback_result(self, feedback_result: Dict[str, Any]) -> ValidationResult:
"""驗證互動回饋結果"""
result = ValidationResult(True, [], [], {})
# 檢查是否有錯誤
if "error" in feedback_result:
result.add_detail("has_error", True)
result.add_detail("error_message", feedback_result["error"])
return result
# 檢查預期字段
expected_fields = ["command_logs", "interactive_feedback", "images"]
for field in expected_fields:
if field not in feedback_result:
result.add_warning(f"互動回饋結果建議包含 {field} 字段")
# 驗證命令日誌
if "command_logs" in feedback_result:
logs = feedback_result["command_logs"]
if not isinstance(logs, str):
result.add_error("command_logs 字段必須是字符串")
# 驗證互動回饋
if "interactive_feedback" in feedback_result:
feedback = feedback_result["interactive_feedback"]
if not isinstance(feedback, str):
result.add_error("interactive_feedback 字段必須是字符串")
elif len(feedback.strip()) == 0:
result.add_warning("interactive_feedback 為空")
# 驗證圖片數據
if "images" in feedback_result:
images = feedback_result["images"]
if not isinstance(images, list):
result.add_error("images 字段必須是數組")
else:
result.add_detail("images_count", len(images))
for i, image in enumerate(images):
if not isinstance(image, dict):
result.add_error(f"圖片 {i} 必須是對象")
continue
if "data" not in image:
result.add_error(f"圖片 {i} 必須包含 data 字段")
if "media_type" not in image:
result.add_error(f"圖片 {i} 必須包含 media_type 字段")
return result
class TestValidators:
"""測試驗證器集合"""
def __init__(self, config: Optional[TestConfig] = None):
self.config = config or DEFAULT_CONFIG
self.message_validator = MCPMessageValidator()
self.result_validator = TestResultValidator(config)
def validate_all(self, test_data: Dict[str, Any]) -> Dict[str, ValidationResult]:
"""驗證所有測試數據"""
results = {}
# 驗證測試結果
if "test_result" in test_data:
results["test_result"] = self.result_validator.validate_test_result(
test_data["test_result"]
)
# 驗證 MCP 消息
if "mcp_messages" in test_data:
message_results = []
for i, message in enumerate(test_data["mcp_messages"]):
msg_result = self.message_validator.validate_json_rpc(message)
msg_result.add_detail("message_index", i)
message_results.append(msg_result)
results["mcp_messages"] = message_results
# 驗證互動回饋結果
if "feedback_result" in test_data:
results["feedback_result"] = self.result_validator.validate_interactive_feedback_result(
test_data["feedback_result"]
)
return results
def get_validation_summary(self, validation_results: Dict[str, ValidationResult]) -> Dict[str, Any]:
"""獲取驗證摘要"""
total_errors = 0
total_warnings = 0
valid_count = 0
total_count = 0
for key, result in validation_results.items():
if isinstance(result, list):
# 處理消息列表
for msg_result in result:
total_errors += len(msg_result.errors)
total_warnings += len(msg_result.warnings)
if msg_result.valid:
valid_count += 1
total_count += 1
else:
# 處理單個結果
total_errors += len(result.errors)
total_warnings += len(result.warnings)
if result.valid:
valid_count += 1
total_count += 1
return {
"total_validations": total_count,
"valid_count": valid_count,
"invalid_count": total_count - valid_count,
"total_errors": total_errors,
"total_warnings": total_warnings,
"success_rate": valid_count / total_count if total_count > 0 else 0
}

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Web UI 模組
===========
提供基於 FastAPI 的 Web 用戶介面,專為 SSH 遠端開發環境設計。
支援文字輸入、圖片上傳、命令執行等功能,並參考 GUI 的設計模式。
"""
from .main import WebUIManager, launch_web_feedback_ui, get_web_ui_manager, stop_web_ui
__all__ = [
'WebUIManager',
'launch_web_feedback_ui',
'get_web_ui_manager',
'stop_web_ui'
]

View File

@@ -0,0 +1,214 @@
{
"app": {
"title": "MCP Interactive Feedback System",
"subtitle": "AI Assistant Interactive Feedback Platform",
"projectDirectory": "Project Directory"
},
"tabs": {
"feedback": "💬 Feedback",
"summary": "📋 AI Summary",
"commands": "⚡ Commands",
"command": "⚡ Commands",
"settings": "⚙️ Settings",
"combined": "📝 Combined Mode",
"about": " About"
},
"feedback": {
"title": "💬 Provide Feedback",
"description": "Please provide your feedback on the AI assistant's work. You can enter text feedback and upload related images.",
"textLabel": "Text Feedback",
"placeholder": "Please enter your feedback here...",
"detailedPlaceholder": "Please enter your feedback here...\n\n💡 Tips:\n• Press Ctrl+Enter/Cmd+Enter (numpad supported) for quick submit\n• Press Ctrl+V/Cmd+V to paste clipboard images directly",
"imageLabel": "Image Attachments (Optional)",
"imageUploadText": "📎 Click to select images or drag and drop images here\nSupports PNG, JPG, JPEG, GIF, BMP, WebP formats",
"submit": "✅ Submit Feedback",
"uploading": "Uploading...",
"dragdrop": "Drag and drop images here or click to upload",
"selectfiles": "Select Files",
"processing": "Processing...",
"success": "Feedback submitted successfully!",
"error": "Error submitting feedback",
"shortcuts": {
"submit": "Ctrl+Enter to submit (Cmd+Enter on Mac, numpad supported)",
"clear": "Ctrl+Delete to clear (Cmd+Delete on Mac)",
"paste": "Ctrl+V to paste images (Cmd+V on Mac)"
}
},
"summary": {
"title": "📋 AI Work Summary",
"description": "Below is the work summary completed by the AI assistant. Please review carefully and provide your feedback.",
"placeholder": "AI work summary will be displayed here...",
"empty": "No summary content available",
"lastupdate": "Last updated",
"refresh": "Refresh"
},
"commands": {
"title": "⚡ Command Execution",
"description": "Execute commands here to verify results or collect more information. Commands will be executed in the project directory.",
"inputLabel": "Command Input",
"placeholder": "Enter command to execute...",
"execute": "▶️ Execute",
"runButton": "▶️ Execute",
"clear": "Clear",
"output": "Command Output",
"outputLabel": "Command Output",
"running": "Running...",
"completed": "Completed",
"error": "Execution Error",
"history": "Command History"
},
"command": {
"title": "⚡ Command Execution",
"description": "Execute commands here to verify results or collect more information. Commands will be executed in the project directory.",
"inputLabel": "Command Input",
"placeholder": "Enter command to execute...",
"execute": "▶️ Execute",
"runButton": "▶️ Execute",
"clear": "Clear",
"output": "Command Output",
"outputLabel": "Command Output",
"running": "Running...",
"completed": "Completed",
"error": "Execution Error",
"history": "Command History"
},
"combined": {
"description": "Combined mode: AI summary and feedback input are on the same page for easy comparison.",
"summaryTitle": "📋 AI Work Summary",
"feedbackTitle": "💬 Provide Feedback"
},
"settings": {
"title": "⚙️ Settings",
"description": "Adjust interface settings and preference options.",
"language": "Language",
"currentLanguage": "Current Language",
"languageDesc": "Select interface display language",
"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",
"autoClose": "Auto Close Page",
"autoCloseDesc": "Automatically close page after submitting feedback",
"theme": "Theme",
"notifications": "Notifications",
"advanced": "Advanced Settings",
"save": "Save Settings",
"reset": "Reset Settings",
"resetDesc": "Clear all saved settings and restore to default state",
"resetConfirm": "Are you sure you want to reset all settings? This will clear all saved preferences.",
"resetSuccess": "Settings have been reset to default values",
"resetError": "Error occurred while resetting settings",
"timeout": "Connection Timeout (seconds)",
"autorefresh": "Auto Refresh",
"debug": "Debug Mode"
},
"languages": {
"zh-TW": "繁體中文",
"zh-CN": "简体中文",
"en": "English"
},
"themes": {
"dark": "Dark",
"light": "Light",
"auto": "Auto"
},
"status": {
"connected": "Connected",
"connecting": "Connecting...",
"disconnected": "Disconnected",
"reconnecting": "Reconnecting...",
"error": "Connection Error",
"waiting": {
"title": "Waiting for Feedback",
"message": "Please provide your feedback"
},
"processing": {
"title": "Processing",
"message": "Submitting your feedback..."
},
"submitted": {
"title": "Feedback Submitted",
"message": "Waiting for next MCP call"
}
},
"notifications": {
"feedback_sent": "Feedback sent",
"command_executed": "Command executed",
"settings_saved": "Settings saved",
"connection_lost": "Connection lost",
"connection_restored": "Connection restored"
},
"connection": {
"waiting": "Connected - Waiting for feedback",
"submitted": "Connected - Feedback submitted",
"processing": "Connected - Processing"
},
"errors": {
"connection_failed": "Connection failed",
"upload_failed": "Upload failed",
"command_failed": "Command execution failed",
"invalid_input": "Invalid input",
"timeout": "Request timeout"
},
"buttons": {
"ok": "OK",
"cancel": "❌ Cancel",
"submit": "✅ Submit Feedback",
"processing": "Processing...",
"submitted": "Submitted",
"retry": "Retry",
"close": "Close",
"upload": "Upload",
"download": "Download"
},
"session": {
"timeout": "⏰ Session has timed out, interface will close automatically",
"timeoutWarning": "Session is about to timeout",
"timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.",
"closing": "Closing..."
},
"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!",
"terminalWelcome": "Welcome to Interactive Feedback Terminal\n========================================\nProject Directory: {sessionId}\nEnter commands and press Enter or click Execute button\nSupported commands: ls, dir, pwd, cat, type, etc.\n\n$ "
},
"about": {
"title": " About",
"description": "A powerful MCP server that provides human-in-the-loop interactive feedback functionality for AI-assisted development tools. Supports both Qt GUI and Web UI interfaces, with rich features including image upload, command execution, multi-language support, and more.",
"appInfo": "Application Information",
"version": "Version",
"projectLinks": "Project Links",
"githubProject": "GitHub Project",
"visitGithub": "Visit GitHub",
"contact": "Contact & Support",
"discordSupport": "Discord Support",
"joinDiscord": "Join Discord",
"contactDescription": "For technical support, issue reports, or feature suggestions, please contact us through Discord community or GitHub Issues.",
"thanks": "Thanks & Contributions",
"thanksText": "Thanks to the original author Fábio Ferreira (@fabiomlferreira) for creating the original interactive-feedback-mcp project.\n\nThis enhanced version is developed and maintained by Minidoracat, greatly expanding the project functionality with GUI interface, image support, multi-language capabilities, and many other improvements.\n\nSpecial thanks to sanshao85's mcp-feedback-collector project for UI design inspiration.\n\nOpen source collaboration makes technology better!"
},
"images": {
"settings": {
"title": "Image Settings",
"sizeLimit": "Image Size Limit",
"sizeLimitOptions": {
"unlimited": "Unlimited",
"1mb": "1MB",
"3mb": "3MB",
"5mb": "5MB"
},
"base64Detail": "Base64 Compatibility Mode",
"base64DetailHelp": "When enabled, includes full Base64 image data in text, improving compatibility with certain AI models",
"base64Warning": "⚠️ Increases transmission size",
"compatibilityHint": "💡 Images not recognized correctly?",
"enableBase64Hint": "Try enabling Base64 compatibility mode"
},
"sizeLimitExceeded": "Image {filename} size is {size}, exceeds {limit} limit!",
"sizeLimitExceededAdvice": "Consider compressing the image with editing software before uploading, or adjust the image size limit settings."
}
}

View File

@@ -0,0 +1,214 @@
{
"app": {
"title": "MCP 交互反馈系统",
"subtitle": "AI 助手交互反馈平台",
"projectDirectory": "项目目录"
},
"tabs": {
"feedback": "💬 反馈",
"summary": "📋 AI 总结",
"commands": "⚡ 命令",
"command": "⚡ 命令",
"settings": "⚙️ 设置",
"combined": "📝 合并模式",
"about": " 关于"
},
"feedback": {
"title": "💬 提供反馈",
"description": "请提供您对 AI 工作成果的反馈意见。您可以输入文字反馈并上传相关图片。",
"textLabel": "文字反馈",
"placeholder": "请在这里输入您的反馈...",
"detailedPlaceholder": "请在这里输入您的反馈...\n\n💡 小提示:\n• 按 Ctrl+Enter/Cmd+Enter (支持数字键盘) 可快速提交\n• 按 Ctrl+V/Cmd+V 可直接粘贴剪贴板图片",
"imageLabel": "图片附件(可选)",
"imageUploadText": "📎 点击选择图片或拖放图片到此处\n支持 PNG、JPG、JPEG、GIF、BMP、WebP 等格式",
"submit": "✅ 提交反馈",
"uploading": "上传中...",
"dragdrop": "拖放图片到这里或点击上传",
"selectfiles": "选择文件",
"processing": "处理中...",
"success": "反馈已成功提交!",
"error": "提交反馈时发生错误",
"shortcuts": {
"submit": "Ctrl+Enter 提交 (Mac 用 Cmd+Enter支持数字键盘)",
"clear": "Ctrl+Delete 清除 (Mac 用 Cmd+Delete)",
"paste": "Ctrl+V 粘贴图片 (Mac 用 Cmd+V)"
}
},
"summary": {
"title": "📋 AI 工作摘要",
"description": "以下是 AI 助手完成的工作摘要,请仔细查看并提供您的反馈意见。",
"placeholder": "AI 工作摘要将在这里显示...",
"empty": "目前没有摘要内容",
"lastupdate": "最后更新",
"refresh": "刷新"
},
"commands": {
"title": "⚡ 命令执行",
"description": "在此执行命令来验证结果或收集更多信息。命令将在项目目录中执行。",
"inputLabel": "命令输入",
"placeholder": "输入要执行的命令...",
"execute": "▶️ 执行",
"runButton": "▶️ 执行",
"clear": "清除",
"output": "命令输出",
"outputLabel": "命令输出",
"running": "执行中...",
"completed": "执行完成",
"error": "执行错误",
"history": "命令历史"
},
"command": {
"title": "⚡ 命令执行",
"description": "在此执行命令来验证结果或收集更多信息。命令将在项目目录中执行。",
"inputLabel": "命令输入",
"placeholder": "输入要执行的命令...",
"execute": "▶️ 执行",
"runButton": "▶️ 执行",
"clear": "清除",
"output": "命令输出",
"outputLabel": "命令输出",
"running": "执行中...",
"completed": "执行完成",
"error": "执行错误",
"history": "命令历史"
},
"combined": {
"description": "合并模式AI 摘要和反馈输入在同一页面中,方便对照查看。",
"summaryTitle": "📋 AI 工作摘要",
"feedbackTitle": "💬 提供反馈"
},
"settings": {
"title": "⚙️ 设定",
"description": "调整界面设定和偏好选项。",
"language": "语言",
"currentLanguage": "当前语言",
"languageDesc": "选择界面显示语言",
"interface": "界面设定",
"layoutMode": "界面布局模式",
"layoutModeDesc": "选择 AI 摘要和反馈输入的显示方式",
"separateMode": "分离模式",
"separateModeDesc": "AI 摘要和反馈分别在不同页签",
"combinedVertical": "合并模式(垂直布局)",
"combinedVerticalDesc": "AI 摘要在上,反馈输入在下,摘要和反馈在同一页面",
"combinedHorizontal": "合并模式(水平布局)",
"combinedHorizontalDesc": "AI 摘要在左,反馈输入在右,增大摘要可视区域",
"autoClose": "自动关闭页面",
"autoCloseDesc": "提交回馈后自动关闭页面",
"theme": "主题",
"notifications": "通知",
"advanced": "进阶设定",
"save": "储存设定",
"reset": "重置设定",
"resetDesc": "清除所有已保存的设定,恢复到预设状态",
"resetConfirm": "确定要重置所有设定吗?这将清除所有已保存的偏好设定。",
"resetSuccess": "设定已重置为预设值",
"resetError": "重置设定时发生错误",
"timeout": "连线逾时 (秒)",
"autorefresh": "自动重新整理",
"debug": "除错模式"
},
"languages": {
"zh-TW": "繁體中文",
"zh-CN": "简体中文",
"en": "English"
},
"themes": {
"dark": "深色",
"light": "浅色",
"auto": "自动"
},
"status": {
"connected": "已连接",
"connecting": "连接中...",
"disconnected": "已断开连接",
"reconnecting": "重新连接中...",
"error": "连接错误",
"waiting": {
"title": "等待反馈",
"message": "请提供您的反馈意见"
},
"processing": {
"title": "处理中",
"message": "正在提交您的反馈..."
},
"submitted": {
"title": "反馈已提交",
"message": "等待下次 MCP 调用"
}
},
"notifications": {
"feedback_sent": "反馈已发送",
"command_executed": "命令已执行",
"settings_saved": "设置已保存",
"connection_lost": "连接中断",
"connection_restored": "连接已恢复"
},
"connection": {
"waiting": "已连接 - 等待反馈",
"submitted": "已连接 - 反馈已提交",
"processing": "已连接 - 处理中"
},
"errors": {
"connection_failed": "连接失败",
"upload_failed": "上传失败",
"command_failed": "命令执行失败",
"invalid_input": "输入内容无效",
"timeout": "请求超时"
},
"buttons": {
"ok": "确定",
"cancel": "❌ 取消",
"submit": "✅ 提交反馈",
"processing": "处理中...",
"submitted": "已提交",
"retry": "重试",
"close": "关闭",
"upload": "上传",
"download": "下载"
},
"session": {
"timeout": "⏰ 会话已超时,界面将自动关闭",
"timeoutWarning": "会话即将超时",
"timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。",
"closing": "正在关闭..."
},
"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请测试这些功能并提供反馈",
"terminalWelcome": "欢迎使用交互反馈终端\n========================================\n项目目录: {sessionId}\n输入命令后按 Enter 或点击执行按钮\n支持的命令: ls, dir, pwd, cat, type 等\n\n$ "
},
"about": {
"title": " 关于",
"description": "一个强大的 MCP 服务器,为 AI 辅助开发工具提供人在回路的交互反馈功能。支持 Qt GUI 和 Web UI 双界面,并具备图片上传、命令执行、多语言等丰富功能。",
"appInfo": "应用程序信息",
"version": "版本",
"projectLinks": "项目链接",
"githubProject": "GitHub 项目",
"visitGithub": "访问 GitHub",
"contact": "联系与支持",
"discordSupport": "Discord 支持",
"joinDiscord": "加入 Discord",
"contactDescription": "如需技术支持、问题反馈或功能建议,欢迎通过 Discord 社群或 GitHub Issues 与我们联系。",
"thanks": "致谢与贡献",
"thanksText": "感谢原作者 Fábio Ferreira (@fabiomlferreira) 创建了原始的 interactive-feedback-mcp 项目。\n\n本增强版本由 Minidoracat 开发和维护,大幅扩展了项目功能,新增了 GUI 界面、图片支持、多语言能力以及许多其他改进功能。\n\n同时感谢 sanshao85 的 mcp-feedback-collector 项目提供的 UI 设计灵感。\n\n开源协作让技术变得更美好"
},
"images": {
"settings": {
"title": "图片设置",
"sizeLimit": "图片大小限制",
"sizeLimitOptions": {
"unlimited": "无限制",
"1mb": "1MB",
"3mb": "3MB",
"5mb": "5MB"
},
"base64Detail": "Base64 兼容模式",
"base64DetailHelp": "启用后会在文本中包含完整的 Base64 图片数据,提升与某些 AI 模型的兼容性",
"base64Warning": "⚠️ 会增加传输量",
"compatibilityHint": "💡 图片无法正确识别?",
"enableBase64Hint": "尝试启用 Base64 兼容模式"
},
"sizeLimitExceeded": "图片 {filename} 大小为 {size},超过 {limit} 限制!",
"sizeLimitExceededAdvice": "建议使用图片编辑软件压缩后再上传,或调整图片大小限制设置。"
}
}

View File

@@ -0,0 +1,214 @@
{
"app": {
"title": "MCP 互動回饋系統",
"subtitle": "AI 助手互動回饋平台",
"projectDirectory": "專案目錄"
},
"tabs": {
"feedback": "💬 回饋",
"summary": "📋 AI 摘要",
"commands": "⚡ 命令",
"command": "⚡ 命令",
"settings": "⚙️ 設定",
"combined": "📝 合併模式",
"about": " 關於"
},
"feedback": {
"title": "💬 提供回饋",
"description": "請提供您對 AI 工作成果的回饋意見。您可以輸入文字回饋並上傳相關圖片。",
"textLabel": "文字回饋",
"placeholder": "請在這裡輸入您的回饋...",
"detailedPlaceholder": "請在這裡輸入您的回饋...\n\n💡 小提示:\n• 按 Ctrl+Enter/Cmd+Enter (支援數字鍵盤) 可快速提交\n• 按 Ctrl+V/Cmd+V 可直接貼上剪貼板圖片",
"imageLabel": "圖片附件(可選)",
"imageUploadText": "📎 點擊選擇圖片或拖放圖片到此處\n支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式",
"submit": "✅ 提交回饋",
"uploading": "上傳中...",
"dragdrop": "拖放圖片到這裡或點擊上傳",
"selectfiles": "選擇檔案",
"processing": "處理中...",
"success": "回饋已成功提交!",
"error": "提交回饋時發生錯誤",
"shortcuts": {
"submit": "Ctrl+Enter 提交 (Mac 用 Cmd+Enter支援數字鍵盤)",
"clear": "Ctrl+Delete 清除 (Mac 用 Cmd+Delete)",
"paste": "Ctrl+V 貼上圖片 (Mac 用 Cmd+V)"
}
},
"summary": {
"title": "📋 AI 工作摘要",
"description": "以下是 AI 助手完成的工作摘要,請仔細查看並提供您的回饋意見。",
"placeholder": "AI 工作摘要將在這裡顯示...",
"empty": "目前沒有摘要內容",
"lastupdate": "最後更新",
"refresh": "重新整理"
},
"commands": {
"title": "⚡ 命令執行",
"description": "在此執行命令來驗證結果或收集更多資訊。命令將在專案目錄中執行。",
"inputLabel": "命令輸入",
"placeholder": "輸入要執行的命令...",
"execute": "▶️ 執行",
"runButton": "▶️ 執行",
"clear": "清除",
"output": "命令輸出",
"outputLabel": "命令輸出",
"running": "執行中...",
"completed": "執行完成",
"error": "執行錯誤",
"history": "命令歷史"
},
"command": {
"title": "⚡ 命令執行",
"description": "在此執行命令來驗證結果或收集更多資訊。命令將在專案目錄中執行。",
"inputLabel": "命令輸入",
"placeholder": "輸入要執行的命令...",
"execute": "▶️ 執行",
"runButton": "▶️ 執行",
"clear": "清除",
"output": "命令輸出",
"outputLabel": "命令輸出",
"running": "執行中...",
"completed": "執行完成",
"error": "執行錯誤",
"history": "命令歷史"
},
"combined": {
"description": "合併模式AI 摘要和回饋輸入在同一頁面中,方便對照查看。",
"summaryTitle": "📋 AI 工作摘要",
"feedbackTitle": "💬 提供回饋"
},
"settings": {
"title": "⚙️ 設定",
"description": "調整介面設定和偏好選項。",
"language": "語言",
"currentLanguage": "當前語言",
"languageDesc": "選擇界面顯示語言",
"interface": "介面設定",
"layoutMode": "界面佈局模式",
"layoutModeDesc": "選擇 AI 摘要和回饋輸入的顯示方式",
"separateMode": "分離模式",
"separateModeDesc": "AI 摘要和回饋分別在不同頁籤",
"combinedVertical": "合併模式(垂直布局)",
"combinedVerticalDesc": "AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面",
"combinedHorizontal": "合併模式(水平布局)",
"combinedHorizontalDesc": "AI 摘要在左,回饋輸入在右,增大摘要可視區域",
"autoClose": "自動關閉頁面",
"autoCloseDesc": "提交回饋後自動關閉頁面",
"theme": "主題",
"notifications": "通知",
"advanced": "進階設定",
"save": "儲存設定",
"reset": "重置設定",
"resetDesc": "清除所有已保存的設定,恢復到預設狀態",
"resetConfirm": "確定要重置所有設定嗎?這將清除所有已保存的偏好設定。",
"resetSuccess": "設定已重置為預設值",
"resetError": "重置設定時發生錯誤",
"timeout": "連線逾時 (秒)",
"autorefresh": "自動重新整理",
"debug": "除錯模式"
},
"languages": {
"zh-TW": "繁體中文",
"zh-CN": "简体中文",
"en": "English"
},
"themes": {
"dark": "深色",
"light": "淺色",
"auto": "自動"
},
"status": {
"connected": "已連線",
"connecting": "連線中...",
"disconnected": "已中斷連線",
"reconnecting": "重新連線中...",
"error": "連線錯誤",
"waiting": {
"title": "等待回饋",
"message": "請提供您的回饋意見"
},
"processing": {
"title": "處理中",
"message": "正在提交您的回饋..."
},
"submitted": {
"title": "回饋已提交",
"message": "等待下次 MCP 調用"
}
},
"notifications": {
"feedback_sent": "回饋已發送",
"command_executed": "指令已執行",
"settings_saved": "設定已儲存",
"connection_lost": "連線中斷",
"connection_restored": "連線已恢復"
},
"connection": {
"waiting": "已連線 - 等待回饋",
"submitted": "已連線 - 反饋已提交",
"processing": "已連線 - 處理中"
},
"errors": {
"connection_failed": "連線失敗",
"upload_failed": "上傳失敗",
"command_failed": "指令執行失敗",
"invalid_input": "輸入內容無效",
"timeout": "請求逾時"
},
"buttons": {
"ok": "確定",
"cancel": "❌ 取消",
"submit": "✅ 提交回饋",
"processing": "處理中...",
"submitted": "已提交",
"retry": "重試",
"close": "關閉",
"upload": "上傳",
"download": "下載"
},
"session": {
"timeout": "⏰ 會話已超時,介面將自動關閉",
"timeoutWarning": "會話即將超時",
"timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。",
"closing": "正在關閉..."
},
"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請測試這些功能並提供回饋",
"terminalWelcome": "歡迎使用互動回饋終端\n========================================\n專案目錄: {sessionId}\n輸入命令後按 Enter 或點擊執行按鈕\n支援的命令: ls, dir, pwd, cat, type 等\n\n$ "
},
"about": {
"title": " 關於",
"description": "一個強大的 MCP 伺服器,為 AI 輔助開發工具提供人在回路的互動回饋功能。支援 Qt GUI 和 Web UI 雙介面,並具備圖片上傳、命令執行、多語言等豐富功能。",
"appInfo": "應用程式資訊",
"version": "版本",
"projectLinks": "專案連結",
"githubProject": "GitHub 專案",
"visitGithub": "訪問 GitHub",
"contact": "聯繫與支援",
"discordSupport": "Discord 支援",
"joinDiscord": "加入 Discord",
"contactDescription": "如需技術支援、問題回報或功能建議,歡迎透過 Discord 社群或 GitHub Issues 與我們聯繫。",
"thanks": "致謝與貢獻",
"thanksText": "感謝原作者 Fábio Ferreira (@fabiomlferreira) 創建了原始的 interactive-feedback-mcp 專案。\n\n本增強版本由 Minidoracat 開發和維護,大幅擴展了專案功能,新增了 GUI 介面、圖片支援、多語言能力以及許多其他改進功能。\n\n同時感謝 sanshao85 的 mcp-feedback-collector 專案提供的 UI 設計靈感。\n\n開源協作讓技術變得更美好"
},
"images": {
"settings": {
"title": "圖片設定",
"sizeLimit": "圖片大小限制",
"sizeLimitOptions": {
"unlimited": "無限制",
"1mb": "1MB",
"3mb": "3MB",
"5mb": "5MB"
},
"base64Detail": "Base64 相容模式",
"base64DetailHelp": "啟用後會在文字中包含完整的 Base64 圖片資料,提升與某些 AI 模型的相容性",
"base64Warning": "⚠️ 會增加傳輸量",
"compatibilityHint": "💡 圖片無法正確識別?",
"enableBase64Hint": "嘗試啟用 Base64 相容模式"
},
"sizeLimitExceeded": "圖片 {filename} 大小為 {size},超過 {limit} 限制!",
"sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。"
}
}

View File

@@ -0,0 +1,500 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Web UI 主要管理器
================
基於 FastAPI 的 Web 用戶介面主要管理類,參考 GUI 的設計模式重構。
專為 SSH 遠端開發環境設計,支援現代化界面和多語言。
"""
import asyncio
import json
import logging
import os
import socket
import threading
import time
import webbrowser
from pathlib import Path
from typing import Dict, Optional
import uuid
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn
from .models import WebFeedbackSession, FeedbackResult
from .routes import setup_routes
from .utils import find_free_port, get_browser_opener
from ..debug import web_debug_log as debug_log
from ..i18n import get_i18n_manager
class WebUIManager:
"""Web UI 管理器 - 重構為單一活躍會話模式"""
def __init__(self, host: str = "127.0.0.1", port: int = None):
self.host = host
# 優先使用固定端口 8765確保 localStorage 的一致性
self.port = port or find_free_port(preferred_port=8765)
self.app = FastAPI(title="MCP Feedback Enhanced")
# 重構:使用單一活躍會話而非會話字典
self.current_session: Optional[WebFeedbackSession] = None
self.sessions: Dict[str, WebFeedbackSession] = {} # 保留用於向後兼容
# 全局標籤頁狀態管理 - 跨會話保持
self.global_active_tabs: Dict[str, dict] = {}
# 會話更新通知標記
self._pending_session_update = False
self.server_thread = None
self.server_process = None
self.i18n = get_i18n_manager()
# 設置靜態文件和模板
self._setup_static_files()
self._setup_templates()
# 設置路由
setup_routes(self)
debug_log(f"WebUIManager 初始化完成,將在 {self.host}:{self.port} 啟動")
def _setup_static_files(self):
"""設置靜態文件服務"""
# Web UI 靜態文件
web_static_path = Path(__file__).parent / "static"
if web_static_path.exists():
self.app.mount("/static", StaticFiles(directory=str(web_static_path)), name="static")
else:
raise RuntimeError(f"Static files directory not found: {web_static_path}")
def _setup_templates(self):
"""設置模板引擎"""
# Web UI 模板
web_templates_path = Path(__file__).parent / "templates"
if web_templates_path.exists():
self.templates = Jinja2Templates(directory=str(web_templates_path))
else:
raise RuntimeError(f"Templates directory not found: {web_templates_path}")
def create_session(self, project_directory: str, summary: str) -> str:
"""創建新的回饋會話 - 重構為單一活躍會話模式,保留標籤頁狀態"""
# 保存舊會話的 WebSocket 連接以便發送更新通知
old_websocket = None
if self.current_session and self.current_session.websocket:
old_websocket = self.current_session.websocket
debug_log("保存舊會話的 WebSocket 連接以發送更新通知")
# 如果已有活躍會話,先保存其標籤頁狀態到全局狀態
if self.current_session:
debug_log("保存現有會話的標籤頁狀態並清理會話")
# 保存標籤頁狀態到全局
if hasattr(self.current_session, 'active_tabs'):
self._merge_tabs_to_global(self.current_session.active_tabs)
# 同步清理會話資源(但保留 WebSocket 連接)
self.current_session._cleanup_sync()
session_id = str(uuid.uuid4())
session = WebFeedbackSession(session_id, project_directory, summary)
# 將全局標籤頁狀態繼承到新會話
session.active_tabs = self.global_active_tabs.copy()
# 設置為當前活躍會話
self.current_session = session
# 同時保存到字典中以保持向後兼容
self.sessions[session_id] = session
debug_log(f"創建新的活躍會話: {session_id}")
debug_log(f"繼承 {len(session.active_tabs)} 個活躍標籤頁")
# 如果有舊的 WebSocket 連接,立即發送會話更新通知
if old_websocket:
self._old_websocket_for_update = old_websocket
self._new_session_for_update = session
debug_log("已保存舊 WebSocket 連接,準備發送會話更新通知")
else:
# 標記需要發送會話更新通知(當新 WebSocket 連接建立時)
self._pending_session_update = True
return session_id
def get_session(self, session_id: str) -> Optional[WebFeedbackSession]:
"""獲取回饋會話 - 保持向後兼容"""
return self.sessions.get(session_id)
def get_current_session(self) -> Optional[WebFeedbackSession]:
"""獲取當前活躍會話"""
return self.current_session
def remove_session(self, session_id: str):
"""移除回饋會話"""
if session_id in self.sessions:
session = self.sessions[session_id]
session.cleanup()
del self.sessions[session_id]
# 如果移除的是當前活躍會話,清空當前會話
if self.current_session and self.current_session.session_id == session_id:
self.current_session = None
debug_log("清空當前活躍會話")
debug_log(f"移除回饋會話: {session_id}")
def clear_current_session(self):
"""清空當前活躍會話"""
if self.current_session:
session_id = self.current_session.session_id
self.current_session.cleanup()
self.current_session = None
# 同時從字典中移除
if session_id in self.sessions:
del self.sessions[session_id]
debug_log("已清空當前活躍會話")
def _merge_tabs_to_global(self, session_tabs: dict):
"""將會話的標籤頁狀態合併到全局狀態"""
current_time = time.time()
expired_threshold = 60 # 60秒過期閾值
# 清理過期的全局標籤頁
self.global_active_tabs = {
tab_id: tab_info
for tab_id, tab_info in self.global_active_tabs.items()
if current_time - tab_info.get('last_seen', 0) <= expired_threshold
}
# 合併會話標籤頁到全局
for tab_id, tab_info in session_tabs.items():
if current_time - tab_info.get('last_seen', 0) <= expired_threshold:
self.global_active_tabs[tab_id] = tab_info
debug_log(f"合併標籤頁狀態,全局活躍標籤頁數量: {len(self.global_active_tabs)}")
def get_global_active_tabs_count(self) -> int:
"""獲取全局活躍標籤頁數量"""
current_time = time.time()
expired_threshold = 60
# 清理過期標籤頁並返回數量
valid_tabs = {
tab_id: tab_info
for tab_id, tab_info in self.global_active_tabs.items()
if current_time - tab_info.get('last_seen', 0) <= expired_threshold
}
self.global_active_tabs = valid_tabs
return len(valid_tabs)
async def broadcast_to_active_tabs(self, message: dict):
"""向所有活躍標籤頁廣播消息"""
if not self.current_session or not self.current_session.websocket:
debug_log("沒有活躍的 WebSocket 連接,無法廣播消息")
return
try:
await self.current_session.websocket.send_json(message)
debug_log(f"已廣播消息到活躍標籤頁: {message.get('type', 'unknown')}")
except Exception as e:
debug_log(f"廣播消息失敗: {e}")
def start_server(self):
"""啟動 Web 伺服器"""
def run_server_with_retry():
max_retries = 5
retry_count = 0
while retry_count < max_retries:
try:
debug_log(f"嘗試啟動伺服器在 {self.host}:{self.port} (嘗試 {retry_count + 1}/{max_retries})")
config = uvicorn.Config(
app=self.app,
host=self.host,
port=self.port,
log_level="warning",
access_log=False
)
server = uvicorn.Server(config)
asyncio.run(server.serve())
break
except OSError as e:
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)
else:
debug_log("已達到最大重試次數,無法啟動伺服器")
break
else:
debug_log(f"伺服器啟動錯誤: {e}")
break
except Exception as e:
debug_log(f"伺服器運行錯誤: {e}")
break
# 在新線程中啟動伺服器
self.server_thread = threading.Thread(target=run_server_with_retry, daemon=True)
self.server_thread.start()
# 等待伺服器啟動
time.sleep(2)
def open_browser(self, url: str):
"""開啟瀏覽器"""
try:
browser_opener = get_browser_opener()
browser_opener(url)
debug_log(f"已開啟瀏覽器:{url}")
except Exception as e:
debug_log(f"無法開啟瀏覽器: {e}")
async def smart_open_browser(self, url: str) -> bool:
"""智能開啟瀏覽器 - 檢測是否已有活躍標籤頁
Returns:
bool: True 表示檢測到活躍標籤頁False 表示開啟了新視窗
"""
import asyncio
import aiohttp
try:
# 檢查是否有活躍標籤頁
has_active_tabs = await self._check_active_tabs()
if has_active_tabs:
debug_log("檢測到活躍標籤頁,不開啟新瀏覽器視窗")
debug_log(f"用戶可以在現有標籤頁中查看更新:{url}")
return True
# 沒有活躍標籤頁,開啟新瀏覽器視窗
debug_log("沒有檢測到活躍標籤頁,開啟新瀏覽器視窗")
self.open_browser(url)
return False
except Exception as e:
debug_log(f"智能瀏覽器開啟失敗,回退到普通開啟:{e}")
self.open_browser(url)
return False
async def notify_session_update(self, session):
"""向活躍標籤頁發送會話更新通知"""
try:
# 向所有活躍的 WebSocket 連接發送會話更新通知
await self.broadcast_to_active_tabs({
"type": "session_updated",
"message": "新會話已創建,正在更新頁面內容",
"session_info": {
"project_directory": session.project_directory,
"summary": session.summary,
"session_id": session.session_id
}
})
debug_log("會話更新通知已發送到所有活躍標籤頁")
except Exception as e:
debug_log(f"發送會話更新通知失敗: {e}")
async def _send_immediate_session_update(self):
"""立即發送會話更新通知(使用舊的 WebSocket 連接)"""
try:
# 檢查是否有保存的舊 WebSocket 連接
if hasattr(self, '_old_websocket_for_update') and hasattr(self, '_new_session_for_update'):
old_websocket = self._old_websocket_for_update
new_session = self._new_session_for_update
# 發送會話更新通知
await old_websocket.send_json({
"type": "session_updated",
"message": "新會話已創建,正在更新頁面內容",
"session_info": {
"project_directory": new_session.project_directory,
"summary": new_session.summary,
"session_id": new_session.session_id
}
})
debug_log("已通過舊 WebSocket 連接發送會話更新通知")
# 清理臨時變數
delattr(self, '_old_websocket_for_update')
delattr(self, '_new_session_for_update')
# 延遲一小段時間讓前端處理消息,然後關閉舊連接
await asyncio.sleep(0.1)
try:
await old_websocket.close()
debug_log("已關閉舊 WebSocket 連接")
except Exception as e:
debug_log(f"關閉舊 WebSocket 連接失敗: {e}")
else:
# 沒有舊連接,設置待更新標記
self._pending_session_update = True
debug_log("沒有舊 WebSocket 連接,設置待更新標記")
except Exception as e:
debug_log(f"立即發送會話更新通知失敗: {e}")
# 回退到待更新標記
self._pending_session_update = True
async def _check_active_tabs(self) -> bool:
"""檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API"""
try:
# 首先檢查全局標籤頁狀態
global_count = self.get_global_active_tabs_count()
if global_count > 0:
debug_log(f"檢測到 {global_count} 個全局活躍標籤頁")
return True
# 如果全局狀態沒有活躍標籤頁,嘗試通過 API 檢查
# 等待一小段時間讓服務器完全啟動
await asyncio.sleep(0.5)
# 調用活躍標籤頁 API
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.get_server_url()}/api/active-tabs", timeout=2) as response:
if response.status == 200:
data = await response.json()
tab_count = data.get("count", 0)
debug_log(f"API 檢測到 {tab_count} 個活躍標籤頁")
return tab_count > 0
else:
debug_log(f"檢查活躍標籤頁失敗,狀態碼:{response.status}")
return False
except asyncio.TimeoutError:
debug_log("檢查活躍標籤頁超時")
return False
except Exception as e:
debug_log(f"檢查活躍標籤頁時發生錯誤:{e}")
return False
def get_server_url(self) -> str:
"""獲取伺服器 URL"""
return f"http://{self.host}:{self.port}"
def stop(self):
"""停止 Web UI 服務"""
# 清理所有會話
for session in list(self.sessions.values()):
session.cleanup()
self.sessions.clear()
# 停止伺服器注意uvicorn 的 graceful shutdown 需要額外處理)
if self.server_thread and self.server_thread.is_alive():
debug_log("正在停止 Web UI 服務")
# 全域實例
_web_ui_manager: Optional[WebUIManager] = None
def get_web_ui_manager() -> WebUIManager:
"""獲取 Web UI 管理器實例"""
global _web_ui_manager
if _web_ui_manager is None:
_web_ui_manager = WebUIManager()
return _web_ui_manager
async def launch_web_feedback_ui(project_directory: str, summary: str, timeout: int = 600) -> dict:
"""
啟動 Web 回饋介面並等待用戶回饋 - 重構為使用根路徑
Args:
project_directory: 專案目錄路徑
summary: AI 工作摘要
timeout: 超時時間(秒)
Returns:
dict: 回饋結果,包含 logs、interactive_feedback 和 images
"""
manager = get_web_ui_manager()
# 創建或更新當前活躍會話
session_id = manager.create_session(project_directory, summary)
session = manager.get_current_session()
if not session:
raise RuntimeError("無法創建回饋會話")
# 啟動伺服器(如果尚未啟動)
if not manager.server_thread or not manager.server_thread.is_alive():
manager.start_server()
# 使用根路徑 URL 並智能開啟瀏覽器
feedback_url = manager.get_server_url() # 直接使用根路徑
has_active_tabs = await manager.smart_open_browser(feedback_url)
debug_log(f"[DEBUG] 服務器地址: {feedback_url}")
# 如果檢測到活躍標籤頁但沒有開啟新視窗,立即發送會話更新通知
if has_active_tabs:
await manager._send_immediate_session_update()
debug_log("已向活躍標籤頁發送會話更新通知")
try:
# 等待用戶回饋,傳遞 timeout 參數
result = await session.wait_for_feedback(timeout)
debug_log(f"收到用戶回饋")
return result
except TimeoutError:
debug_log(f"會話超時")
# 資源已在 wait_for_feedback 中清理,這裡只需要記錄和重新拋出
raise
except Exception as e:
debug_log(f"會話發生錯誤: {e}")
raise
finally:
# 注意:不再自動清理會話和停止服務器,保持持久性
# 會話將保持活躍狀態,等待下次 MCP 調用
debug_log("會話保持活躍狀態,等待下次 MCP 調用")
def stop_web_ui():
"""停止 Web UI 服務"""
global _web_ui_manager
if _web_ui_manager:
_web_ui_manager.stop()
_web_ui_manager = None
debug_log("Web UI 服務已停止")
# 測試用主函數
if __name__ == "__main__":
async def main():
try:
project_dir = os.getcwd()
summary = "這是一個測試摘要,用於驗證 Web UI 功能。"
from ..debug import debug_log
debug_log(f"啟動 Web UI 測試...")
debug_log(f"專案目錄: {project_dir}")
debug_log("等待用戶回饋...")
result = await launch_web_feedback_ui(project_dir, summary)
debug_log("收到回饋結果:")
debug_log(f"命令日誌: {result.get('logs', '')}")
debug_log(f"互動回饋: {result.get('interactive_feedback', '')}")
debug_log(f"圖片數量: {len(result.get('images', []))}")
except KeyboardInterrupt:
debug_log("\n用戶取消操作")
except Exception as e:
debug_log(f"錯誤: {e}")
finally:
stop_web_ui()
asyncio.run(main())

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Web UI 資料模型模組
==================
定義 Web UI 相關的資料結構和型別。
"""
from .feedback_session import WebFeedbackSession
from .feedback_result import FeedbackResult
__all__ = [
'WebFeedbackSession',
'FeedbackResult'
]

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Web UI 回饋結果資料模型
======================
定義回饋收集的資料結構,與 GUI 版本保持一致。
"""
from typing import TypedDict, List
class FeedbackResult(TypedDict):
"""回饋結果的型別定義"""
command_logs: str
interactive_feedback: str
images: List[dict]

Some files were not shown because too many files have changed in this diff Show More