first commit
15
.bumpversion.cfg
Normal 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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
3.11
|
||||
24
LICENSE
Normal 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
@@ -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
@@ -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 並分享給更多開發者!**
|
||||
190
RELEASE_NOTES/CHANGELOG.en.md
Normal 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)
|
||||
213
RELEASE_NOTES/CHANGELOG.zh-CN.md
Normal 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)
|
||||
190
RELEASE_NOTES/CHANGELOG.zh-TW.md
Normal 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
@@ -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
@@ -0,0 +1,24 @@
|
||||
# Release vX.Y.Z - [發佈標題]
|
||||
|
||||
## 🌟 亮點
|
||||
本次發佈主要變更的簡要摘要。
|
||||
|
||||
## ✨ 新功能
|
||||
- 🆕 **功能名稱**: 新功能的描述
|
||||
- 🎨 **界面增強**: 介面改進的描述
|
||||
|
||||
## 🐛 錯誤修復
|
||||
- 🔧 **問題修復**: 修復內容的描述 (fixes #issue_number)
|
||||
- 🛡️ **安全修復**: 安全相關的改進
|
||||
|
||||
## 🚀 改進功能
|
||||
- ⚡ **效能優化**: 效能最佳化
|
||||
- 📱 **用戶體驗增強**: 用戶體驗改進
|
||||
|
||||
## 🔄 變更
|
||||
- 🔀 **重大變更**: 任何重大變更(僅限主要版本)
|
||||
- 📝 **文檔更新**: 文檔更新
|
||||
|
||||
---
|
||||
**說明**: 此模板應該適應每種語言(CHANGELOG.en.md, CHANGELOG.zh-TW.md, CHANGELOG.zh-CN.md)
|
||||
**注意**: 版本發佈文件不包含安裝與相關連結部分,這些內容已移至各語言的完整 CHANGELOG 文件中
|
||||
28
RELEASE_NOTES/v2.2.1/en.md
Normal 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)
|
||||
28
RELEASE_NOTES/v2.2.1/zh-CN.md
Normal 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 (部分完成)
|
||||
28
RELEASE_NOTES/v2.2.1/zh-TW.md
Normal 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 (部分完成)
|
||||
30
RELEASE_NOTES/v2.2.2/en.md
Normal 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)
|
||||
30
RELEASE_NOTES/v2.2.2/zh-CN.md
Normal 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)
|
||||
30
RELEASE_NOTES/v2.2.2/zh-TW.md
Normal 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)
|
||||
42
RELEASE_NOTES/v2.2.3/en.md
Normal 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)
|
||||
42
RELEASE_NOTES/v2.2.3/zh-CN.md
Normal 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 (图片设置功能)
|
||||
42
RELEASE_NOTES/v2.2.3/zh-TW.md
Normal 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 (圖片設定功能)
|
||||
23
RELEASE_NOTES/v2.2.4/en.md
Normal 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)
|
||||
23
RELEASE_NOTES/v2.2.4/zh-CN.md
Normal 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)
|
||||
23
RELEASE_NOTES/v2.2.4/zh-TW.md
Normal 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)
|
||||
28
RELEASE_NOTES/v2.2.5/en.md
Normal 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)
|
||||
28
RELEASE_NOTES/v2.2.5/zh-CN.md
Normal 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)
|
||||
28
RELEASE_NOTES/v2.2.5/zh-TW.md
Normal 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
@@ -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
@@ -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
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/en/images/gui2.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/en/images/web1.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
docs/en/images/web2.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
142
docs/zh-CN/cache-management.md
Normal 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
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/zh-CN/images/gui2.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/zh-CN/images/web1.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
docs/zh-CN/images/web2.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
142
docs/zh-TW/cache-management.md
Normal 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
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/zh-TW/images/gui2.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/zh-TW/images/web1.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
docs/zh-TW/images/web2.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
60
pyproject.toml
Normal 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
@@ -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
@@ -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
@@ -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()
|
||||
57
src/mcp_feedback_enhanced/__init__.py
Normal 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()
|
||||
168
src/mcp_feedback_enhanced/__main__.py
Normal 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()
|
||||
85
src/mcp_feedback_enhanced/debug.py
Normal 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"
|
||||
25
src/mcp_feedback_enhanced/gui/__init__.py
Normal 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']
|
||||
172
src/mcp_feedback_enhanced/gui/locales/README.md
Normal 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
|
||||
|
||||
需要幫助可以參考現有的翻譯檔案作為範本。
|
||||
207
src/mcp_feedback_enhanced/gui/locales/en/translations.json
Normal 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!"
|
||||
}
|
||||
}
|
||||
202
src/mcp_feedback_enhanced/gui/locales/zh-CN/translations.json
Normal 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开源协作让技术变得更美好!"
|
||||
}
|
||||
}
|
||||
202
src/mcp_feedback_enhanced/gui/locales/zh-TW/translations.json
Normal 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請測試這些功能並提供回饋!"
|
||||
}
|
||||
}
|
||||
147
src/mcp_feedback_enhanced/gui/main.py
Normal 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()
|
||||
10
src/mcp_feedback_enhanced/gui/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
GUI 資料模型模組
|
||||
===============
|
||||
|
||||
定義 GUI 相關的資料結構和型別。
|
||||
"""
|
||||
|
||||
from .feedback import FeedbackResult
|
||||
|
||||
__all__ = ['FeedbackResult']
|
||||
17
src/mcp_feedback_enhanced/gui/models/feedback.py
Normal 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]
|
||||
17
src/mcp_feedback_enhanced/gui/styles/__init__.py
Normal 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'
|
||||
]
|
||||
277
src/mcp_feedback_enhanced/gui/styles/themes.py
Normal 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;
|
||||
}
|
||||
"""
|
||||
22
src/mcp_feedback_enhanced/gui/tabs/__init__.py
Normal 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'
|
||||
]
|
||||
261
src/mcp_feedback_enhanced/gui/tabs/about_tab.py
Normal 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'))
|
||||
194
src/mcp_feedback_enhanced/gui/tabs/command_tab.py
Normal 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()
|
||||
168
src/mcp_feedback_enhanced/gui/tabs/feedback_tab.py
Normal 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)
|
||||
623
src/mcp_feedback_enhanced/gui/tabs/settings_tab.py
Normal 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)
|
||||
130
src/mcp_feedback_enhanced/gui/tabs/summary_tab.py
Normal 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'))
|
||||
14
src/mcp_feedback_enhanced/gui/utils/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
GUI 工具函數模組
|
||||
===============
|
||||
|
||||
包含各種輔助工具函數。
|
||||
"""
|
||||
|
||||
from .shortcuts import setup_shortcuts
|
||||
from .utils import apply_widget_styles
|
||||
|
||||
__all__ = [
|
||||
'setup_shortcuts',
|
||||
'apply_widget_styles'
|
||||
]
|
||||
35
src/mcp_feedback_enhanced/gui/utils/shortcuts.py
Normal 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)
|
||||
50
src/mcp_feedback_enhanced/gui/utils/utils.py
Normal 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"
|
||||
19
src/mcp_feedback_enhanced/gui/widgets/__init__.py
Normal 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'
|
||||
]
|
||||
95
src/mcp_feedback_enhanced/gui/widgets/image_preview.py
Normal 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)
|
||||
740
src/mcp_feedback_enhanced/gui/widgets/image_upload.py
Normal 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()
|
||||
163
src/mcp_feedback_enhanced/gui/widgets/styled_spinbox.py
Normal 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)
|
||||
237
src/mcp_feedback_enhanced/gui/widgets/switch.py
Normal 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"};
|
||||
}}
|
||||
""")
|
||||
37
src/mcp_feedback_enhanced/gui/widgets/text_edit.py
Normal 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)
|
||||
322
src/mcp_feedback_enhanced/gui/widgets/timeout_widget.py
Normal 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;
|
||||
}}
|
||||
"""
|
||||
20
src/mcp_feedback_enhanced/gui/window/__init__.py
Normal 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'
|
||||
]
|
||||
242
src/mcp_feedback_enhanced/gui/window/command_executor.py
Normal 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()
|
||||
217
src/mcp_feedback_enhanced/gui/window/config_manager.py
Normal 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:
|
||||
"""獲取圖片大小限制(bytes),0 表示無限制"""
|
||||
return self.get('image_size_limit', 0)
|
||||
|
||||
def set_image_size_limit(self, size_bytes: int) -> None:
|
||||
"""設置圖片大小限制(bytes),0 表示無限制"""
|
||||
# 處理 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
|
||||
791
src/mcp_feedback_enhanced/gui/window/feedback_window.py
Normal 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("主窗口已關閉")
|
||||
358
src/mcp_feedback_enhanced/gui/window/tab_manager.py
Normal 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}")
|
||||
360
src/mcp_feedback_enhanced/i18n.py
Normal 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()
|
||||
675
src/mcp_feedback_enhanced/server.py
Normal 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 表示可以使用 GUI,False 表示只能使用 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()
|
||||
286
src/mcp_feedback_enhanced/test_mcp_enhanced.py
Normal 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())
|
||||
100
src/mcp_feedback_enhanced/test_qt_gui.py
Normal 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)
|
||||
375
src/mcp_feedback_enhanced/test_web_ui.py
Normal 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)
|
||||
37
src/mcp_feedback_enhanced/testing/__init__.py
Normal 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"
|
||||
133
src/mcp_feedback_enhanced/testing/config.py
Normal 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()
|
||||
527
src/mcp_feedback_enhanced/testing/mcp_client.py
Normal 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 客戶端資源已清理")
|
||||
447
src/mcp_feedback_enhanced/testing/reporter.py
Normal 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)
|
||||
469
src/mcp_feedback_enhanced/testing/scenarios.py
Normal 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
|
||||
}
|
||||
266
src/mcp_feedback_enhanced/testing/utils.py
Normal 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)
|
||||
394
src/mcp_feedback_enhanced/testing/validators.py
Normal 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
|
||||
}
|
||||
18
src/mcp_feedback_enhanced/web/__init__.py
Normal 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'
|
||||
]
|
||||
214
src/mcp_feedback_enhanced/web/locales/en/translation.json
Normal 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."
|
||||
}
|
||||
}
|
||||
214
src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
Normal 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": "建议使用图片编辑软件压缩后再上传,或调整图片大小限制设置。"
|
||||
}
|
||||
}
|
||||
214
src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
Normal 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": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。"
|
||||
}
|
||||
}
|
||||
500
src/mcp_feedback_enhanced/web/main.py
Normal 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())
|
||||
16
src/mcp_feedback_enhanced/web/models/__init__.py
Normal 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'
|
||||
]
|
||||
17
src/mcp_feedback_enhanced/web/models/feedback_result.py
Normal 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]
|
||||