Compare commits

...

49 Commits
v2.5.3 ... main

Author SHA1 Message Date
Minidoracat
541ca1e5db
Merge pull request #138 from DF-wu/main
[Doc] Localized and Enhancement. (Issue #135)
2025-06-29 21:00:20 +08:00
D.F.
05742bd92d Doc: remove redundant space for saving token. 2025-06-29 17:56:56 +08:00
D.F.
57e6ed2fa6 Doc: modify the prompt align its language. 2025-06-29 17:47:16 +08:00
D.F.
a0e964518c Doc: Localized and enhancement. 2025-06-29 17:36:18 +08:00
D.F.
4abf2a5e1c Doc: Correct the typo. 2025-06-29 17:29:32 +08:00
Minidoracat
5ed5001d59 Merge branch 'main' of github.com:Minidoracat/mcp-feedback-enhanced 2025-06-29 00:29:14 +08:00
Minidoracat
cf977e27a8 📝 更新文檔,增加語系環境變數說明 2025-06-29 00:28:59 +08:00
GitHub Action
9e3b365ca6 🔖 Release v2.6.0
- Updated version to 2.6.0
- Custom version specified: 2.6.0
- Auto-generated release from workflow
2025-06-28 14:36:53 +00:00
Minidoracat
577f16abf9 📝 更新2.6.0相關文檔 2025-06-28 22:35:35 +08:00
Minidoracat
99baa9202c 測試模式端口自動切換 2025-06-28 22:13:07 +08:00
Minidoracat
4328909670 🔥 刪除多餘檔案 2025-06-28 21:45:18 +08:00
Minidoracat
cb6edccbb4 新增新建會話和提交後自動執行命令功能 2025-06-28 21:44:08 +08:00
Minidoracat
867f29e2e9 🐛 修復命令功能 2025-06-28 20:31:59 +08:00
Minidoracat
441df85e1e 🔨 重新調整會話超時機制、增加設定功能 2025-06-28 07:10:15 +08:00
Minidoracat
60e64c90dc 🎨 重構多語系機制,讓通知也支持多語系 2025-06-28 06:22:52 +08:00
Minidoracat
0917214272 會話管理頁籤增加匯出相關功能 2025-06-28 01:30:08 +08:00
Minidoracat
5e103f10d8 自動提交倒數增加暫停和開始按鈕 2025-06-28 01:16:16 +08:00
Minidoracat
2ec789280a 增加系統通知功能 2025-06-28 00:57:38 +08:00
Minidoracat
7b6b177031 🎨 大幅簡化UI 2025-06-27 22:48:02 +08:00
Minidoracat
5f9eb6a42e 🐛 修復 WebSocket 狀態檢測導入錯誤 (#78) 2025-06-27 19:01:05 +08:00
Minidoracat
de6838c79c 🐛 修復 Copilot 審查問題並完善會話歷史 i18n 支援
- 修復 fallbackCopyTextToClipboard 函數定義順序錯誤
  - 修復會話歷史區塊語言切換不生效問題
  - 修復 renderHistory 函數名稱錯誤
  - 添加複製按鈕的多語言支援
  - 優化調試日誌管理,添加 DEBUG_MODE 控制
  - 統一程式碼註釋為繁體中文
2025-06-27 11:47:11 +08:00
Minidoracat
788d7b61cc
Merge pull request #130 from agassiz/main
web交互体验修改
2025-06-27 11:45:13 +08:00
李振民
bc87d4b580 用户信息保存到服务器 2025-06-26 11:36:09 +08:00
李振民
33c94b79be 完美弹出窗口 2025-06-26 10:27:18 +08:00
李振民
44b197a2bc 完成 稳定版本 2025-06-26 09:58:55 +08:00
李振民
2cd8d91bb9 测试 2025-06-26 09:43:16 +08:00
李振民
9c412a1321 消息修改 2025-06-26 01:31:47 +08:00
李振民
4dcf449c99 1、添加复制消息功能;2 资源加载 优化,全使用本地资源 2025-06-25 17:48:05 +08:00
李振民
fab9003b98 复制当前用户内容 2025-06-25 15:38:17 +08:00
GitHub Action
6c421a24b2 🔖 Release v2.5.6
- Updated version to 2.5.6
- Custom version specified: 2.5.6
- Auto-generated release from workflow
2025-06-21 19:06:21 +00:00
Minidoracat
ac53c8e5dd 📝 更新2.5.6相關文檔 2025-06-22 03:05:13 +08:00
Minidoracat
be573a9a95 🔨 重構了保存機制,移除 localStorage 相關 2025-06-22 02:49:20 +08:00
Minidoracat
81c6177b02 設定頁籤內的各功能加上圖標 2025-06-22 02:38:51 +08:00
GitHub Action
d324069e76 🔖 Release v2.5.5
- Updated version to 2.5.5
- Custom version specified: 2.5.5
- Auto-generated release from workflow
2025-06-21 14:42:33 +00:00
Minidoracat
56f0938870 📝 更新 2.5.5 相關文檔 2025-06-21 22:41:52 +08:00
GitHub Action
5f951ddaba 🖥️ 更新桌面應用二進制文件 - 自動構建多平台版本 (Run 15796466490) 2025-06-21 14:11:24 +00:00
Minidoracat
475f286c05
Allow set host via environment (#113)
Fix SSH remote development issue by adding MCP_WEB_HOST environment variable

- Add MCP_WEB_HOST environment variable to configure web server host binding
- Default to 127.0.0.1 for security, allow 0.0.0.0 for remote access
- Update documentation across all language versions
- Update configuration examples
- Maintain backward compatibility

Fixes #88

Co-authored-by: leo108 <leo108.luo@gmail.com>
2025-06-21 21:58:19 +08:00
Minidoracat
6450953f01 將 interactive_feedback 的 docstring 改為全英文 2025-06-21 21:37:55 +08:00
fireinice
f2262db207
feat: add prompt instructions into tool docstring (#105)
* Optimize token efficiency by moving LLM instructions to tool docstring
* Simplify user configuration by removing complex Cursor rules
* Improve tool self-documentation following FastMCP best practices

Co-authored-by: fireinice <fireinice@users.noreply.github.com>
2025-06-21 21:17:13 +08:00
Minidoracat
6903314a4d 🐛 修正桌面應用 MCP 協議污染問題 2025-06-21 21:06:43 +08:00
Minidoracat
5b407061af
Merge pull request #93 from Alsan/main
feat: 新增 macOS PyO3 編譯配置支援

- 新增 .cargo/config.toml 配置 macOS 鏈接器參數
- 支援 Intel (x86_64) 和 Apple Silicon (aarch64) 架構
- 解決 macOS 上 PyO3 undefined dynamic_lookup 編譯問題
- 遵循 PyO3 官方推薦的 macOS 配置最佳實踐

感謝 @Alsan 的寶貴貢獻!這將大大改善 macOS 用戶的開發體驗。
2025-06-21 21:04:39 +08:00
leo108
df82c6bd5d Allow set host via environment 2025-06-18 14:00:29 +00:00
alsan
05a8709b9e
build(desktop): files generated, may ignore? 2025-06-17 08:21:26 +08:00
alsan
9b0070da92
fix(src-tauri): add config for build macos 2025-06-17 08:20:06 +08:00
Minidoracat
1c6e8856be 🔥 移除 esc 快捷功能 2025-06-16 15:00:56 +08:00
Minidoracat
9a07a0dbab 🔥 移除舊版回饋分頁相關程式碼 2025-06-16 14:53:52 +08:00
Minidoracat
5e47c1dfa6 📝 更新說明文檔 Star History 2025-06-15 20:42:51 +08:00
GitHub Action
078620bd0a 🔖 Release v2.5.4
- Updated version to 2.5.4
- Custom version specified: 2.5.4
- Auto-generated release from workflow
2025-06-15 11:58:57 +00:00
Minidoracat
d165579cab 🐛 修正打包後查詢執行檔邏輯和增加調試訊息 2025-06-15 19:58:10 +08:00
73 changed files with 6457 additions and 1215 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.5.3
current_version = 2.6.0
commit = False
tag = False
allow_dirty = True

115
README.md
View File

@ -8,11 +8,11 @@
## 🎯 Core Concept
This is an [MCP server](https://modelcontextprotocol.io/) that establishes **feedback-oriented development workflows**, providing **Web UI and Desktop Application** dual interface options, perfectly adapting to local, **SSH Remote environments** (Cursor SSH Remote, VS Code Remote SSH), and **WSL (Windows Subsystem for Linux) environments**. By guiding AI to confirm with users rather than making speculative operations, it can consolidate multiple tool calls into a single feedback-oriented request, dramatically reducing platform costs and improving development efficiency.
This is an [MCP server](https://modelcontextprotocol.io/) that establishes **feedback-oriented development workflows**, providing **Web UI and Desktop Application** dual interface options, perfectly adapting to local, **SSH Remote environments**, and **WSL (Windows Subsystem for Linux) environments**. By guiding AI to confirm with users rather than making speculative operations, it can consolidate multiple tool calls into a single feedback-oriented request, dramatically reducing platform costs and improving development efficiency.
**🌐 Dual Interface Architecture Advantages:**
- 🖥️ **Desktop Application**: Native cross-platform desktop experience, supporting Windows, macOS, Linux
- 🌐 **Web UI Interface**: No GUI dependencies required, suitable for remote and WSL environments
- 🌐 **Web UI**: No GUI dependencies required, suitable for remote and WSL environments
- 🔧 **Flexible Deployment**: Choose the most suitable interface mode based on environment requirements
- 📦 **Unified Functionality**: Both interfaces provide exactly the same functional experience
@ -38,14 +38,16 @@ This is an [MCP server](https://modelcontextprotocol.io/) that establishes **fee
### 📝 Smart Workflow
- **Prompt Management**: CRUD operations for common prompts, usage statistics, intelligent sorting
- **Auto-Timed Submit**: 1-86400 second flexible timer, supports pause, resume, cancel
- **Session Management & Tracking**: Local file storage, privacy controls, history export, real-time statistics
- **Auto-Timed Submit**: 1-86400 second flexible timer, supports pause, resume, cancel with new pause/resume button controls
- **Auto Command Execution** (v2.6.0): Automatically execute preset commands after creating new sessions or commits for improved development efficiency
- **Session Management & Tracking**: Local file storage, privacy controls, history export (supports JSON, CSV, Markdown formats), real-time statistics, flexible timeout settings
- **Connection Monitoring**: WebSocket status monitoring, auto-reconnection, quality indicators
- **AI Work Summary Markdown Display**: Support for rich Markdown syntax rendering including headers, bold text, code blocks, lists, links and other formats for enhanced content readability
### 🎨 Modern Experience
- **Responsive Design**: Adapts to different screen sizes, modular JavaScript architecture
- **Audio Notifications**: Built-in multiple sound effects, custom audio upload support, volume control
- **System Notifications** (v2.6.0): System-level real-time alerts for important events (like auto-commit, session timeout)
- **Smart Memory**: Input box height memory, one-click copy, persistent settings
- **Multi-language Support**: Traditional Chinese, English, Simplified Chinese, instant switching
@ -59,7 +61,7 @@ This is an [MCP server](https://modelcontextprotocol.io/) that establishes **fee
### Web UI Interface (v2.5.0 - Desktop Application Support)
<div align="center">
<img src="docs/en/images/web1.jpeg" width="400" alt="Web UI Main Interface - Prompt Management & Auto Submit" />
<img src="docs/en/images/web1.png" width="400" alt="Web UI Main Interface - Prompt Management & Auto Submit" />
</div>
<details>
@ -92,9 +94,6 @@ This is an [MCP server](https://modelcontextprotocol.io/) that establishes **fee
```bash
# Install uv (if not already installed)
pip install uv
# Quick test
uvx mcp-feedback-enhanced@latest test
```
### 2. Configure MCP
@ -122,7 +121,9 @@ uvx mcp-feedback-enhanced@latest test
"timeout": 600,
"env": {
"MCP_DEBUG": "false",
"MCP_WEB_PORT": "8765"
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_LANGUAGE": "en"
},
"autoApprove": ["interactive_feedback"]
}
@ -140,6 +141,7 @@ uvx mcp-feedback-enhanced@latest test
"timeout": 600,
"env": {
"MCP_DESKTOP_MODE": "true",
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_DEBUG": "false"
},
@ -159,11 +161,7 @@ For optimal results, add the following rules to your AI assistant:
```
# MCP Interactive Feedback Rules
1. During any process, task, or conversation, whether asking, replying, or completing phased tasks, you must call MCP mcp-feedback-enhanced.
2. Whenever user feedback is received, if the feedback content is not empty, you must call MCP mcp-feedback-enhanced again and adjust behavior based on the feedback content.
3. Only when the user explicitly indicates "end" or "no more interaction needed" can you stop calling MCP mcp-feedback-enhanced, and the process is considered complete.
4. Unless receiving termination instructions, all steps must repeatedly call MCP mcp-feedback-enhanced.
5. Before completing tasks, you must use the MCP mcp-feedback-enhanced tool to ask users for feedback.
follow mcp-feedback-enhanced instructions
```
## ⚙️ Advanced Settings
@ -172,8 +170,27 @@ For optimal results, add the following rules to your AI assistant:
| Variable | Purpose | Values | Default |
|----------|---------|--------|---------|
| `MCP_DEBUG` | Debug mode | `true`/`false` | `false` |
| `MCP_WEB_HOST` | Web UI host binding | IP address or hostname | `127.0.0.1` |
| `MCP_WEB_PORT` | Web UI port | `1024-65535` | `8765` |
| `MCP_DESKTOP_MODE` | Desktop application mode | `true`/`false` | `false` |
| `MCP_LANGUAGE` | Force UI language | `zh-TW`/`zh-CN`/`en` | Auto-detect |
**`MCP_WEB_HOST` Explanation**:
- `127.0.0.1` (default): Local access only, higher security
- `0.0.0.0`: Allow remote access, suitable for SSH remote development environments
**`MCP_LANGUAGE` Explanation**:
- Used to force the interface language, overriding automatic system detection
- Supported language codes:
- `zh-TW`: Traditional Chinese
- `zh-CN`: Simplified Chinese
- `en`: English
- Language detection priority:
1. User-saved language settings in the interface (highest priority)
2. `MCP_LANGUAGE` environment variable
3. System environment variables (LANG, LC_ALL, etc.)
4. System default language
5. Fallback to default language (Traditional Chinese)
### Testing Options
```bash
@ -186,6 +203,11 @@ uvx mcp-feedback-enhanced@latest test --desktop # Test desktop application (v2.5
# Debug mode
MCP_DEBUG=true uvx mcp-feedback-enhanced@latest test
# Specify language for testing
MCP_LANGUAGE=en uvx mcp-feedback-enhanced@latest test --web # Force English interface
MCP_LANGUAGE=zh-TW uvx mcp-feedback-enhanced@latest test --web # Force Traditional Chinese
MCP_LANGUAGE=zh-CN uvx mcp-feedback-enhanced@latest test --web # Force Simplified Chinese
```
### Developer Installation
@ -231,23 +253,51 @@ make quick-check # Quick check and auto-f
## 🆕 Version History
📋 **Complete Version History:** [RELEASE_NOTES/CHANGELOG.md](RELEASE_NOTES/CHANGELOG.md)
📋 **Complete Version History:** [RELEASE_NOTES/CHANGELOG.en.md](RELEASE_NOTES/CHANGELOG.en.md)
### Latest Version Highlights (v2.5.0)
- 🖥️ **Desktop Application**: Brand new cross-platform desktop application supporting Windows, macOS, Linux
- 📋 **AI Work Summary Markdown Display**: Support for Markdown syntax rendering including headers, bold text, code blocks, lists, links and other formats
- ⚡ **Significant Performance Enhancement**: Introduced debounce/throttle mechanisms to reduce unnecessary rendering and network requests
- 📊 **Session History Storage Improvement**: Migrated from localStorage to server-side local file storage
- 🌐 **Network Connection Stability**: Improved WebSocket reconnection mechanism with network status detection
- 🎨 **UI Rendering Optimization**: Optimized rendering performance for session management, statistics, and status indicators
- 🛠️ **Build Process Optimization**: Added Makefile desktop application build commands and development tools
- 📚 **Documentation Enhancement**: Added desktop application build guide and workflow documentation
### Latest Version Highlights (v2.6.0)
- 🚀 **Auto Command Execution**: Automatically execute preset commands after creating new sessions or commits, improving workflow efficiency
- 📊 **Session Export Feature**: Support exporting session records to multiple formats for easy sharing and archiving
- ⏸️ **Auto-commit Control**: Added pause and resume buttons for better control over auto-commit timing
- 🔔 **System Notifications**: System-level notifications for important events with real-time alerts
- ⏱️ **Session Timeout Optimization**: Redesigned session management with more flexible configuration options
- 🌏 **I18n Enhancement**: Refactored internationalization architecture with full multilingual support for notifications
- 🎨 **UI Simplification**: Significantly simplified user interface for improved user experience
## 🐛 Common Issues
### 🌐 SSH Remote Environment Issues
**Q: Browser cannot launch in SSH Remote environment**
A: This is normal. SSH Remote environments have no graphical interface, requiring manual opening in local browser. For detailed solutions, refer to: [SSH Remote Environment Usage Guide](docs/en/ssh-remote/browser-launch-issues.md)
**Q: Browser cannot launch or access in SSH Remote environment**
A: Two solutions available:
**Solution 1: Environment Variable Setting (v2.5.5 Recommended)**
Set `"MCP_WEB_HOST": "0.0.0.0"` in MCP configuration to allow remote access:
```json
{
"mcpServers": {
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"env": {
"MCP_WEB_HOST": "0.0.0.0",
"MCP_WEB_PORT": "8765"
},
"autoApprove": ["interactive_feedback"]
}
}
}
```
Then open in local browser: `http://[remote-host-IP]:8765`
**Solution 2: SSH Port Forwarding (Traditional Method)**
1. Use default configuration (`MCP_WEB_HOST`: `127.0.0.1`)
2. Set up SSH port forwarding:
- **VS Code Remote SSH**: Press `Ctrl+Shift+P` → "Forward a Port" → Enter `8765`
- **Cursor SSH Remote**: Manually add port forwarding rule (port 8765)
3. Open in local browser: `http://localhost:8765`
For detailed solutions, refer to: [SSH Remote Environment Usage Guide](docs/en/ssh-remote/browser-launch-issues.md)
**Q: Why am I not receiving new MCP feedback?**
A: Likely a WebSocket connection issue. **Solution**: Directly refresh the browser page.
@ -341,6 +391,15 @@ If you find it useful, please:
**penn201500** - [GitHub @penn201500](https://github.com/penn201500)
- 🎯 Auto-focus input box feature ([PR #39](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/39))
**leo108** - [GitHub @leo108](https://github.com/leo108)
- 🌐 SSH Remote Development Support (`MCP_WEB_HOST` environment variable) ([PR #113](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/113))
**Alsan** - [GitHub @Alsan](https://github.com/Alsan)
- 🍎 macOS PyO3 Compilation Configuration Support ([PR #93](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/93))
**fireinice** - [GitHub @fireinice](https://github.com/fireinice)
- 📝 Tool Documentation Optimization (LLM instructions moved to docstring) ([PR #105](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/105))
### Community Support
- **Discord:** [https://discord.gg/Gur2V67](https://discord.gg/Gur2V67)
- **Issues:** [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues)
@ -349,5 +408,9 @@ If you find it useful, please:
MIT License - See [LICENSE](LICENSE) file for details
## 📈 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Minidoracat/mcp-feedback-enhanced&type=Date)](https://star-history.com/#Minidoracat/mcp-feedback-enhanced&Date)
---
**🌟 Welcome to Star and share with more developers!**

View File

@ -8,7 +8,7 @@
## 🎯 核心概念
这是一个 [MCP 服务器](https://modelcontextprotocol.io/),建立**反馈导向的开发工作流程**,提供**Web UI 和桌面应用程序**双重选择,完美适配本地、**SSH Remote 环境**Cursor SSH Remote、VS Code Remote SSH**WSL (Windows Subsystem for Linux) 环境**。通过引导 AI 与用户确认而非进行推测性操作,可将多次工具调用合并为单次反馈导向请求,大幅节省平台成本并提升开发效率。
这是一个 [MCP 服务器](https://modelcontextprotocol.io/),建立**反馈导向的开发工作流程**,提供**Web UI 和桌面应用程序**双重选择,完美适配本地、**SSH 远程开发环境****WSL (Windows Subsystem for Linux) 环境**。通过引导 AI 与用户确认而非进行推测性操作,可将多次工具调用合并为单次反馈导向请求,大幅节省平台成本并提升开发效率。
**🌐 双重界面架构优势:**
- 🖥️ **桌面应用程序**:原生跨平台桌面体验,支持 Windows、macOS、Linux
@ -38,15 +38,17 @@
### 📝 智能工作流程
- **提示词管理**:常用提示词的 CRUD 操作、使用统计、智能排序
- **自动定时提交**1-86400 秒弹性计时器,支持暂停、恢复、取消
- **会话管理追踪**:本地文件存储、隐私控制、历史导出、即时统计
- **自动定时提交**1-86400 秒弹性计时器,支持暂停、恢复、取消,新增暂停/开始按钮控制
- **自动执行命令**v2.6.0):新建会话和提交后可自动执行预设命令,提升开发效率
- **会话管理追踪**:本地文件存储、隐私控制、历史导出(支持 JSON、CSV、Markdown 格式)、即时统计、弹性超时设定
- **连接监控**WebSocket 状态监控、自动重连、品质指示
- **AI 工作摘要 Markdown 显示**:支持丰富的 Markdown 语法渲染,包含标题、粗体、代码区块、列表、链接等格式,提升内容可读性
### 🎨 现代化体验
- **响应式设计**:适配不同屏幕尺寸,模块化 JavaScript 架构
- **音效通知**:内建多种音效、支持自定义音效上传、音量控制
- **智能记忆**:输入框高度记忆、一键复制、设定持久化
- **系统通知**v2.6.0):重要事件(如自动提交、会话超时等)的系统级即时提醒
- **智能记忆**:输入框高度记忆、一键复制、设置持久化
- **多语言支持**:简体中文、英文、繁体中文,即时切换
### 🖼️ 图片与媒体
@ -59,7 +61,7 @@
### Web UI 界面v2.5.0 - 支持桌面应用程序)
<div align="center">
<img src="docs/zh-CN/images/web1.jpeg" width="400" alt="Web UI 主界面 - 提示词管理与自动提交" />
<img src="docs/zh-CN/images/web1.png" width="400" alt="Web UI 主界面 - 提示词管理与自动提交" />
</div>
<details>
@ -92,9 +94,6 @@
```bash
# 安装 uv如果尚未安装
pip install uv
# 快速测试
uvx mcp-feedback-enhanced@latest test
```
### 2. 配置 MCP
@ -122,7 +121,9 @@ uvx mcp-feedback-enhanced@latest test
"timeout": 600,
"env": {
"MCP_DEBUG": "false",
"MCP_WEB_PORT": "8765"
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_LANGUAGE": "zh-CN"
},
"autoApprove": ["interactive_feedback"]
}
@ -140,6 +141,7 @@ uvx mcp-feedback-enhanced@latest test
"timeout": 600,
"env": {
"MCP_DESKTOP_MODE": "true",
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_DEBUG": "false"
},
@ -153,27 +155,42 @@ uvx mcp-feedback-enhanced@latest test
- 桌面模式:[examples/mcp-config-desktop.json](examples/mcp-config-desktop.json)
- Web 模式:[examples/mcp-config-web.json](examples/mcp-config-web.json)
### 3. 设提示工程
### 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 工具向用户询问反馈。
注意在任务完成前根据mcp-feedback-enhanced工具的要求进行工具调用
```
## ⚙️ 进阶设定
## ⚙️ 高级设置
### 环境变量
| 变量 | 用途 | 值 | 默认 |
|------|------|-----|------|
| `MCP_DEBUG` | 调试模式 | `true`/`false` | `false` |
| `MCP_WEB_HOST` | Web UI 主机绑定 | IP 地址或主机名 | `127.0.0.1` |
| `MCP_WEB_PORT` | Web UI 端口 | `1024-65535` | `8765` |
| `MCP_DESKTOP_MODE` | 桌面应用程序模式 | `true`/`false` | `false` |
| `MCP_LANGUAGE` | 强制指定界面语言 | `zh-TW`/`zh-CN`/`en` | 自动检测 |
**`MCP_WEB_HOST` 说明**
- `127.0.0.1`(默认):仅本地访问,安全性较高
- `0.0.0.0`:允许远程访问,适用于 SSH 远程开发环境
**`MCP_LANGUAGE` 说明**
- 用于强制指定界面语言,覆盖系统自动检测
- 支持的语言代码:
- `zh-TW`:繁体中文
- `zh-CN`:简体中文
- `en`:英文
- 语言检测优先顺序:
1. 用户在界面中保存的语言设置(最高优先级)
2. `MCP_LANGUAGE` 环境变量
3. 系统环境变量LANG、LC_ALL 等)
4. 系统默认语言
5. 回退到默认语言(繁体中文)
### 测试选项
```bash
@ -186,6 +203,11 @@ uvx mcp-feedback-enhanced@latest test --desktop # 测试桌面应用程序 (v2.5
# 调试模式
MCP_DEBUG=true uvx mcp-feedback-enhanced@latest test
# 指定语言测试
MCP_LANGUAGE=en uvx mcp-feedback-enhanced@latest test --web # 强制使用英文界面
MCP_LANGUAGE=zh-TW uvx mcp-feedback-enhanced@latest test --web # 强制使用繁体中文
MCP_LANGUAGE=zh-CN uvx mcp-feedback-enhanced@latest test --web # 强制使用简体中文
```
### 开发者安装
@ -233,21 +255,49 @@ make quick-check # 快速检查并自动
📋 **完整版本更新记录:** [RELEASE_NOTES/CHANGELOG.zh-CN.md](RELEASE_NOTES/CHANGELOG.zh-CN.md)
### 最新版本亮点v2.5.0
- 🖥️ **桌面应用程序**: 全新跨平台桌面应用,支持 Windows、macOS、Linux
- 📋 **AI 工作摘要 Markdown 显示**: 支持 Markdown 语法渲染,包含标题、粗体、代码区块、列表、链接等格式
- ⚡ **性能大幅提升**: 引入防抖/节流机制,减少不必要的渲染和网络请求
- 📊 **会话历史存储改进**: 从 localStorage 改为服务器端本地文件存储
- 🌐 **网络连接稳定性**: 改进 WebSocket 重连机制,支持网络状态检测
- 🎨 **UI 渲染优化**: 优化会话管理、统计信息、状态指示器的渲染性能
- 🛠️ **构建流程优化**: 新增 Makefile 桌面应用构建命令和开发工具
- 📚 **文档完善**: 新增桌面应用构建指南和工作流程说明
### 最新版本亮点v2.6.0
- 🚀 **自动执行命令**: 新建会话和提交后可自动执行预设命令,提升工作效率
- 📊 **会话导出功能**: 支持将会话记录导出为多种格式,方便分享和存档
- ⏸️ **自动提交控制**: 新增暂停和开始按钮,让用户更好控制自动提交时机
- 🔔 **系统通知**: 新增系统级通知功能,重要事件即时提醒
- ⏱️ **会话超时机制优化**: 重新设计会话管理,提供更弹性的设置选项
- 🌏 **多语系强化**: 重构多语系架构,通知系统也完整支持多语言
- 🎨 **界面简化**: 大幅简化用户界面,提升使用体验
## 🐛 常见问题
### 🌐 SSH Remote 环境问题
**Q: SSH Remote 环境下浏览器无法启动**
A: 这是正常现象。SSH Remote 环境没有图形界面,需要手动在本地浏览器打开。详细解决方案请参考:[SSH Remote 环境使用指南](docs/zh-CN/ssh-remote/browser-launch-issues.md)
**Q: SSH Remote 环境下浏览器无法启动或无法访问**
A: 提供两种解决方案:
**方案一环境变量设置v2.5.5 推荐)**
在 MCP 配置中设置 `"MCP_WEB_HOST": "0.0.0.0"` 允许远程访问:
```json
{
"mcpServers": {
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"env": {
"MCP_WEB_HOST": "0.0.0.0",
"MCP_WEB_PORT": "8765"
},
"autoApprove": ["interactive_feedback"]
}
}
}
```
然后在本地浏览器打开:`http://[远程主机IP]:8765`
**方案二SSH 端口转发(传统方法)**
1. 使用默认配置(`MCP_WEB_HOST`: `127.0.0.1`
2. 设置 SSH 端口转发:
- **VS Code Remote SSH**: 按 `Ctrl+Shift+P` → "Forward a Port" → 输入 `8765`
- **Cursor SSH Remote**: 手动添加端口转发规则(端口 8765
3. 在本地浏览器打开:`http://localhost:8765`
详细解决方案请参考:[SSH Remote 环境使用指南](docs/zh-CN/ssh-remote/browser-launch-issues.md)
**Q: 为什么没有接收到 MCP 新的反馈?**
A: 可能是 WebSocket 连接问题。**解决方法**:直接重新刷新浏览器页面。
@ -341,6 +391,15 @@ A: 各种 AI 模型(包括 Gemini Pro 2.5、Claude 等)在图片解析上可
**penn201500** - [GitHub @penn201500](https://github.com/penn201500)
- 🎯 自动聚焦输入框功能 ([PR #39](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/39))
**leo108** - [GitHub @leo108](https://github.com/leo108)
- 🌐 SSH 远程开发支持 (`MCP_WEB_HOST` 环境变量) ([PR #113](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/113))
**Alsan** - [GitHub @Alsan](https://github.com/Alsan)
- 🍎 macOS PyO3 编译配置支持 ([PR #93](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/93))
**fireinice** - [GitHub @fireinice](https://github.com/fireinice)
- 📝 工具文档优化 (LLM 指令移至 docstring) ([PR #105](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/105))
### 社群支援
- **Discord** [https://discord.gg/Gur2V67](https://discord.gg/Gur2V67)
- **Issues** [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues)
@ -349,5 +408,9 @@ A: 各种 AI 模型(包括 Gemini Pro 2.5、Claude 等)在图片解析上可
MIT 授权条款 - 详见 [LICENSE](LICENSE) 档案
## 📈 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Minidoracat/mcp-feedback-enhanced&type=Date)](https://star-history.com/#Minidoracat/mcp-feedback-enhanced&Date)
---
**🌟 欢迎 Star 并分享给更多开发者!**

View File

@ -38,20 +38,22 @@
### 📝 智能工作流程
- **提示詞管理**:常用提示詞的 CRUD 操作、使用統計、智能排序
- **自動定時提交**1-86400 秒彈性計時器,支援暫停、恢復、取消
- **會話管理追蹤**:本地檔案存儲、隱私控制、歷史匯出、即時統計
- **自動定時提交**1-86400 秒彈性計時器,支援暫停、恢復、取消,新增暫停/開始按鈕控制
- **自動執行命令**v2.6.0):新建會話和提交後可自動執行預設命令,提升開發效率
- **會話管理追蹤**:本地檔案存儲、隱私控制、歷史匯出(支援 JSON、CSV、Markdown 格式)、即時統計、彈性超時設定
- **連線監控**WebSocket 狀態監控、自動重連、品質指示
- **AI 工作摘要 Markdown 顯示**:支援豐富的 Markdown 語法渲染,包含標題、粗體、程式碼區塊、列表、連結等格式,提升內容可讀性
### 🎨 現代化體驗
- **響應式設計**:適配不同螢幕尺寸,模組化 JavaScript 架構
- **音效通知**:內建多種音效、支援自訂音效上傳、音量控制
- **系統通知**v2.6.0):重要事件(如自動提交、會話超時等)的系統級即時提醒
- **智能記憶**:輸入框高度記憶、一鍵複製、設定持久化
- **多語言支援**:繁體中文、英文、簡體中文,即時切換
### 🖼️ 圖片與媒體
- **全格式支援**PNG、JPG、JPEG、GIF、BMP、WebP
- **便捷上傳**:拖拽檔案、剪貼板Ctrl+V
- **便捷上傳**:拖拽檔案、剪貼板貼Ctrl+V
- **無限制處理**:支援任意大小圖片,自動智能處理
## 🌐 介面預覽
@ -59,7 +61,7 @@
### Web UI 介面v2.5.0 - 支援桌面應用程式)
<div align="center">
<img src="docs/zh-TW/images/web1.jpeg" width="400" alt="Web UI 主介面 - 提示詞管理與自動提交" />
<img src="docs/zh-TW/images/web1.png" width="400" alt="Web UI 主介面 - 提示詞管理與自動提交" />
</div>
<details>
@ -83,7 +85,7 @@
**快捷鍵支援**
- `Ctrl+Enter`Windows/Linux/ `Cmd+Enter`macOS提交回饋主鍵盤與數字鍵盤皆支援
- `Ctrl+V`Windows/Linux/ `Cmd+V`macOS直接貼剪貼板圖片
- `Ctrl+V`Windows/Linux/ `Cmd+V`macOS直接貼剪貼板圖片
- `Ctrl+I`Windows/Linux/ `Cmd+I`macOS快速聚焦輸入框 (感謝 @penn201500)
## 🚀 快速開始
@ -92,9 +94,6 @@
```bash
# 安裝 uv如果尚未安裝
pip install uv
# 快速測試
uvx mcp-feedback-enhanced@latest test
```
### 2. 配置 MCP
@ -122,7 +121,9 @@ uvx mcp-feedback-enhanced@latest test
"timeout": 600,
"env": {
"MCP_DEBUG": "false",
"MCP_WEB_PORT": "8765"
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_LANGUAGE": "zh-TW"
},
"autoApprove": ["interactive_feedback"]
}
@ -140,6 +141,7 @@ uvx mcp-feedback-enhanced@latest test
"timeout": 600,
"env": {
"MCP_DESKTOP_MODE": "true",
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_DEBUG": "false"
},
@ -159,21 +161,36 @@ uvx mcp-feedback-enhanced@latest test
```
# 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 工具向使用者詢問回饋。
注意在任務完成前根據mcp-feedback-enhanced工具的要求進行工具調用
```
## ⚙️ 進階設定
### 環境變數
| 變數 | 用途 | 值 | 默認 |
| 變數 | 用途 | 值 | 預設 |
|------|------|-----|------|
| `MCP_DEBUG` | 調試模式 | `true`/`false` | `false` |
| `MCP_WEB_HOST` | Web UI 主機綁定 | IP 地址或主機名 | `127.0.0.1` |
| `MCP_WEB_PORT` | Web UI 端口 | `1024-65535` | `8765` |
| `MCP_DESKTOP_MODE` | 桌面應用程式模式 | `true`/`false` | `false` |
| `MCP_LANGUAGE` | 強制指定介面語言 | `zh-TW`/`zh-CN`/`en` | 自動偵測 |
**`MCP_WEB_HOST` 說明**
- `127.0.0.1`(預設):僅本地存取,安全性較高
- `0.0.0.0`:允許遠端存取,適用於 SSH 遠端開發環境
**`MCP_LANGUAGE` 說明**
- 用於強制指定介面語言,覆蓋系統自動偵測
- 支援的語言代碼:
- `zh-TW`:繁體中文
- `zh-CN`:簡體中文
- `en`:英文
- 語言偵測優先順序:
1. 用戶在介面中保存的語言設定(最高優先級)
2. `MCP_LANGUAGE` 環境變數
3. 系統環境變數LANG、LC_ALL 等)
4. 系統預設語言
5. 回退到預設語言(繁體中文)
### 測試選項
```bash
@ -186,6 +203,11 @@ uvx mcp-feedback-enhanced@latest test --desktop # 測試桌面應用程式 (v2.5
# 調試模式
MCP_DEBUG=true uvx mcp-feedback-enhanced@latest test
# 指定語言測試
MCP_LANGUAGE=en uvx mcp-feedback-enhanced@latest test --web # 強制使用英文介面
MCP_LANGUAGE=zh-TW uvx mcp-feedback-enhanced@latest test --web # 強制使用繁體中文
MCP_LANGUAGE=zh-CN uvx mcp-feedback-enhanced@latest test --web # 強制使用簡體中文
```
### 開發者安裝
@ -234,21 +256,49 @@ make quick-check # 快速檢查並自動
📋 **完整版本更新記錄:** [RELEASE_NOTES/CHANGELOG.zh-TW.md](RELEASE_NOTES/CHANGELOG.zh-TW.md)
### 最新版本亮點v2.5.0
- 🖥️ **桌面應用程式**: 全新跨平台桌面應用,支援 Windows、macOS、Linux
- 📋 **AI 工作摘要 Markdown 顯示**: 支援 Markdown 語法渲染,包含標題、粗體、程式碼區塊、列表、連結等格式
- ⚡ **效能大幅提升**: 引入防抖/節流機制,減少不必要的渲染和網路請求
- 📊 **會話歷史存儲改進**: 從 localStorage 改為伺服器端本地檔案存儲
- 🌐 **網路連接穩定性**: 改進 WebSocket 重連機制,支援網路狀態檢測
- 🎨 **UI 渲染優化**: 優化會話管理、統計資訊、狀態指示器的渲染效能
- 🛠️ **構建流程優化**: 新增 Makefile 桌面應用構建命令和開發工具
- 📚 **文檔完善**: 新增桌面應用構建指南和工作流程說明
### 最新版本亮點v2.6.0
- 🚀 **自動執行命令**: 新建會話和提交後可自動執行預設命令,提升工作效率
- 📊 **會話匯出功能**: 支援將會話記錄匯出為多種格式,方便分享和存檔
- ⏸️ **自動提交控制**: 新增暫停和開始按鈕,讓使用者更好控制自動提交時機
- 🔔 **系統通知**: 新增系統級通知功能,重要事件即時提醒
- ⏱️ **會話超時機制優化**: 重新設計會話管理,提供更彈性的設定選項
- 🌏 **多語系強化**: 重構多語系架構,通知系統也完整支援多語言
- 🎨 **介面簡化**: 大幅簡化使用者介面,提升使用體驗
## 🐛 常見問題
### 🌐 SSH Remote 環境問題
**Q: SSH Remote 環境下瀏覽器無法啟動**
A: 這是正常現象。SSH Remote 環境沒有圖形界面,需要手動在本地瀏覽器開啟。詳細解決方案請參考:[SSH Remote 環境使用指南](docs/zh-TW/ssh-remote/browser-launch-issues.md)
**Q: SSH Remote 環境下瀏覽器無法啟動或無法存取**
A: 提供兩種解決方案:
**方案一環境變數設定v2.5.5 推薦)**
在 MCP 配置中設定 `"MCP_WEB_HOST": "0.0.0.0"` 允許遠端存取:
```json
{
"mcpServers": {
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"env": {
"MCP_WEB_HOST": "0.0.0.0",
"MCP_WEB_PORT": "8765"
},
"autoApprove": ["interactive_feedback"]
}
}
}
```
然後在本地瀏覽器開啟:`http://[遠端主機IP]:8765`
**方案二SSH 端口轉發(傳統方法)**
1. 使用預設配置(`MCP_WEB_HOST`: `127.0.0.1`
2. 設定 SSH 端口轉發:
- **VS Code Remote SSH**: 按 `Ctrl+Shift+P` → "Forward a Port" → 輸入 `8765`
- **Cursor SSH Remote**: 手動添加端口轉發規則(端口 8765
3. 在本地瀏覽器開啟:`http://localhost:8765`
詳細解決方案請參考:[SSH Remote 環境使用指南](docs/zh-TW/ssh-remote/browser-launch-issues.md)
**Q: 為什麼沒有接收到 MCP 新的反饋?**
A: 可能是 WebSocket 連接問題。**解決方法**:直接重新整理瀏覽器頁面。
@ -342,6 +392,15 @@ A: 各種 AI 模型(包括 Gemini Pro 2.5、Claude 等)在圖片解析上可
**penn201500** - [GitHub @penn201500](https://github.com/penn201500)
- 🎯 自動聚焦輸入框功能 ([PR #39](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/39))
**leo108** - [GitHub @leo108](https://github.com/leo108)
- 🌐 SSH 遠端開發支援 (`MCP_WEB_HOST` 環境變數) ([PR #113](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/113))
**Alsan** - [GitHub @Alsan](https://github.com/Alsan)
- 🍎 macOS PyO3 編譯配置支援 ([PR #93](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/93))
**fireinice** - [GitHub @fireinice](https://github.com/fireinice)
- 📝 工具文檔優化 (LLM 指令移至 docstring) ([PR #105](https://github.com/Minidoracat/mcp-feedback-enhanced/pull/105))
### 社群支援
- **Discord** [https://discord.gg/Gur2V67](https://discord.gg/Gur2V67)
- **Issues** [GitHub Issues](https://github.com/Minidoracat/mcp-feedback-enhanced/issues)
@ -350,5 +409,9 @@ A: 各種 AI 模型(包括 Gemini Pro 2.5、Claude 等)在圖片解析上可
MIT 授權條款 - 詳見 [LICENSE](LICENSE) 檔案
## 📈 Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Minidoracat/mcp-feedback-enhanced&type=Date)](https://star-history.com/#Minidoracat/mcp-feedback-enhanced&Date)
---
**🌟 歡迎 Star 並分享給更多開發者!**

View File

@ -2,6 +2,65 @@
This document records all version updates for **MCP Feedback Enhanced**.
## [v2.5.6] - 2025-06-21 - Settings Save Mechanism Optimization & Interface Enhancement
### 🌟 Version Highlights
Refactored settings save mechanism to resolve language switching save issues, and added visual icons to settings interface for enhanced user experience.
### 🚀 Improvements
- 🔨 **Settings Save Mechanism Refactoring**: Completely removed localStorage dependency, switched to unified FastAPI backend save mechanism
- Resolved settings not saving correctly during language switching
- Removed debounce mechanism to ensure immediate settings save
- Enhanced reliability and consistency of settings save
- ✨ **Settings Interface Enhancement**: Added corresponding icons to functional sections within settings tabs
- Improved interface visual effects and user experience
- More intuitive feature identification
### 🛠️ Technical Improvements
- 📊 **Unified Storage Architecture**: All settings now use JSON file storage for cross-environment consistency
- 🔧 **Code Simplification**: Removed complex localStorage-related code, reducing maintenance costs
---
## [v2.5.5] - 2025-06-21 - SSH Remote Development Support & Stability Enhancement
### 🌟 Version Highlights
Added SSH remote development environment support, resolving Web UI access issues in remote development scenarios. Enhanced macOS compilation support and desktop application stability for improved developer experience.
### ✨ New Features
- 🌐 **SSH Remote Development Support**: Added `MCP_WEB_HOST` environment variable for configuring web server host binding
- Defaults to `127.0.0.1` for security
- Can be set to `0.0.0.0` to allow remote access
- Resolves access issues in remote development environments like Cursor SSH Remote
- 🍎 **Enhanced macOS Compilation Support**: Added `.cargo/config.toml` configuration file
- Supports Intel (x86_64) and Apple Silicon (aarch64) architectures
- Resolves macOS PyO3 undefined dynamic_lookup compilation issues
- Follows PyO3 official recommended best practices
### 🚀 Improvements
- 📝 **Tool Documentation Optimization**: Moved LLM instructions to tool docstring for improved token efficiency
- 🎨 **Simplified User Configuration**: Removed complex Cursor rules configuration
- 📊 **Enhanced AI Work Summary Markdown**: Improved Markdown rendering effects and compatibility
- 🔄 **Session History Process Optimization**: Enhanced session saving and management mechanisms
### 🐛 Bug Fixes
- 🖥️ **Desktop Application MCP Protocol Fix**: Fixed MCP protocol communication pollution issues in desktop mode
- 📦 **Packaging Process Fix**: Fixed multi-platform desktop application packaging and publishing issues
- 🔧 **Release Process Optimization**: Improved stability of automated release workflows
- 🔥 **Removed ESC Shortcut**: Removed ESC shortcut functionality that could cause accidental closure
### 🛠️ Technical Improvements
- 🏗️ **Enhanced Build System**: Improved cross-platform compilation configuration and dependency management
- 📚 **Documentation Automation**: Enhanced tool self-documentation following FastMCP best practices
- 🔍 **Enhanced Debugging Features**: Added more detailed debugging information and error handling
### 📋 Usage Instructions
- **SSH Remote Development**: Set `"MCP_WEB_HOST": "0.0.0.0"` in MCP configuration to allow remote access
- **Local Development**: Keep default `"MCP_WEB_HOST": "127.0.0.1"` for security
- **macOS Development**: New compilation configuration will take effect automatically without additional setup
---
## [v2.5.0] - 2025-06-15 - Desktop Application & Performance Optimization
### 🌟 Version Highlights

View File

@ -2,6 +2,29 @@
This document records all version updates for **MCP Feedback Enhanced**.
## [v2.6.0] - 2025-06-28 - Intelligent Session Management & Automation Enhancement
### 🌟 Version Highlights
Significantly enhanced session management capabilities with automatic command execution, export features, and notification system, providing a more intelligent development experience.
### ✨ New Features
- 🚀 **Auto Command Execution**: Automatically execute preset commands after creating new sessions or commits
- 📊 **Session Export Feature**: Support exporting session records to multiple formats
- ⏸️ **Auto-commit Control**: Added pause and resume buttons for better control over auto-commit timing
- 🔔 **System Notifications**: System-level notifications for important events with real-time alerts
### 🚀 Improvements
- ⏱️ **Session Timeout Optimization**: Redesigned session management with more flexible configuration options
- 🌏 **I18n Enhancement**: Refactored internationalization architecture with full multilingual support for notifications
- 🎨 **UI Simplification**: Significantly simplified user interface for improved user experience
### 🐛 Bug Fixes
- Fixed command execution functionality issues
- Fixed WebSocket status detection import errors
- Improved session history multilingual support
---
## [v2.5.0] - 2025-06-15 - Desktop Application & Performance Optimization
### 🌟 Version Highlights

View File

@ -2,6 +2,88 @@
本文件记录了 **MCP Feedback Enhanced** 的所有版本更新内容。
## [v2.6.0] - 2025-06-28 - 智能会话管理与自动化功能强化
### 🌟 版本亮点
大幅强化会话管理功能,新增自动执行命令、导出功能和通知系统,提供更智能的开发体验。
### ✨ 新功能
- 🚀 **自动执行命令**: 新建会话和提交后可自动执行预设命令
- 📊 **会话导出功能**: 支持将会话记录导出为多种格式
- ⏸️ **自动提交控制**: 新增暂停和开始按钮,让用户更好控制自动提交时机
- 🔔 **系统通知**: 新增系统级通知功能,重要事件即时提醒
### 🚀 改进功能
- ⏱️ **会话超时机制优化**: 重新设计会话管理,提供更弹性的设置选项
- 🌏 **多语系强化**: 重构多语系架构,通知系统也完整支持多语言
- 🎨 **界面简化**: 大幅简化用户界面,提升使用体验
### 🐛 问题修复
- 修复命令执行功能的相关问题
- 修正 WebSocket 状态检测的导入错误
- 完善会话历史的多语言支持
---
## [v2.5.6] - 2025-06-21 - 设置保存机制优化与界面美化
### 🌟 版本亮点
重构设置保存机制,解决语系切换保存问题,并为设置界面增加可视化图标,提升用户体验。
### 🚀 改进功能
- 🔨 **设置保存机制重构**: 完全移除 localStorage 依赖,改用统一的 FastAPI 后端保存机制
- 解决语系切换时设置无法正确保存的问题
- 移除防抖机制,确保设置即时保存
- 提升设置保存的可靠性和一致性
- ✨ **设置界面美化**: 为设置页签内的各功能区块新增对应图标
- 提升界面视觉效果和用户体验
- 更直观的功能识别
### 🛠️ 技术改进
- 📊 **统一存储架构**: 所有设置统一使用 JSON 文件存储,确保跨环境一致性
- 🔧 **代码简化**: 移除复杂的 localStorage 相关代码,降低维护成本
---
## [v2.5.5] - 2025-06-21 - SSH 远程开发支持与稳定性增强
### 🌟 版本亮点
新增 SSH 远程开发环境支持,解决远程开发时无法访问 Web UI 的问题。同时改进 macOS 编译支持和桌面应用稳定性,提升开发者体验。
### ✨ 新功能
- 🌐 **SSH 远程开发支持**: 新增 `MCP_WEB_HOST` 环境变量,支持设置 Web 服务器主机绑定
- 默认为 `127.0.0.1` 确保安全性
- 可设置为 `0.0.0.0` 允许远程访问
- 解决 Cursor SSH Remote 等远程开发环境的访问问题
- 🍎 **macOS 编译支持增强**: 新增 `.cargo/config.toml` 配置文件
- 支持 Intel (x86_64) 和 Apple Silicon (aarch64) 架构
- 解决 macOS 上 PyO3 undefined dynamic_lookup 编译问题
- 遵循 PyO3 官方推荐的最佳实践
### 🚀 改进功能
- 📝 **工具文档优化**: 将 LLM 指令移至工具 docstring提升 token 效率
- 🎨 **用户配置简化**: 移除复杂的 Cursor 规则配置
- 📊 **AI 工作摘要 Markdown 增强**: 改进 Markdown 渲染效果和兼容性
- 🔄 **会话历史流程优化**: 改进会话保存和管理机制
### 🐛 问题修复
- 🖥️ **桌面应用 MCP 协议修复**: 修正桌面模式下 MCP 协议通信污染问题
- 📦 **打包流程修复**: 修正多平台桌面应用打包和发布问题
- 🔧 **发布流程优化**: 改进自动化发布工作流程的稳定性
- 🔥 **移除 ESC 快捷键**: 移除可能造成意外关闭的 ESC 快捷键功能
### 🛠️ 技术改进
- 🏗️ **构建系统增强**: 改进跨平台编译配置和依赖管理
- 📚 **文档自动化**: 改进工具自我文档化,遵循 FastMCP 最佳实践
- 🔍 **调试功能增强**: 新增更详细的调试信息和错误处理
### 📋 使用说明
- **SSH 远程开发**: 在 MCP 配置中设置 `"MCP_WEB_HOST": "0.0.0.0"` 允许远程访问
- **本地开发**: 保持默认 `"MCP_WEB_HOST": "127.0.0.1"` 确保安全性
- **macOS 开发**: 新的编译配置将自动生效,无需额外设置
---
## [v2.5.0] - 2025-06-15 - 桌面应用程序与性能优化
### 🌟 版本亮点

View File

@ -2,6 +2,88 @@
本文件記錄了 **MCP Feedback Enhanced** 的所有版本更新內容。
## [v2.6.0] - 2025-06-28 - 智能會話管理與自動化功能強化
### 🌟 版本亮點
大幅強化會話管理功能,新增自動執行命令、匯出功能和通知系統,提供更智能的開發體驗。
### ✨ 新功能
- 🚀 **自動執行命令**: 新建會話和提交後可自動執行預設命令
- 📊 **會話匯出功能**: 支援將會話記錄匯出為多種格式
- ⏸️ **自動提交控制**: 新增暫停和開始按鈕,讓使用者更好控制自動提交時機
- 🔔 **系統通知**: 新增系統級通知功能,重要事件即時提醒
### 🚀 改進功能
- ⏱️ **會話超時機制優化**: 重新設計會話管理,提供更彈性的設定選項
- 🌏 **多語系強化**: 重構多語系架構,通知系統也完整支援多語言
- 🎨 **介面簡化**: 大幅簡化使用者介面,提升使用體驗
### 🐛 問題修復
- 修復命令執行功能的相關問題
- 修正 WebSocket 狀態檢測的導入錯誤
- 完善會話歷史的多語言支援
---
## [v2.5.6] - 2025-06-21 - 設定保存機制優化與介面美化
### 🌟 版本亮點
重構設定保存機制,解決語系切換保存問題,並為設定介面增加視覺化圖標,提升使用者體驗。
### 🚀 改進功能
- 🔨 **設定保存機制重構**: 完全移除 localStorage 依賴,改用統一的 FastAPI 後端保存機制
- 解決語系切換時設定無法正確保存的問題
- 移除防抖機制,確保設定即時保存
- 提升設定保存的可靠性和一致性
- ✨ **設定介面美化**: 為設定頁籤內的各功能區塊新增對應圖標
- 提升介面視覺效果和使用者體驗
- 更直觀的功能識別
### 🛠️ 技術改進
- 📊 **統一存儲架構**: 所有設定統一使用 JSON 檔案存儲,確保跨環境一致性
- 🔧 **程式碼簡化**: 移除複雜的 localStorage 相關程式碼,降低維護成本
---
## [v2.5.5] - 2025-06-21 - SSH 遠端開發支援與穩定性增強
### 🌟 版本亮點
新增 SSH 遠端開發環境支援,解決遠端開發時無法存取 Web UI 的問題。同時改進 macOS 編譯支援和桌面應用穩定性,提升開發者體驗。
### ✨ 新功能
- 🌐 **SSH 遠端開發支援**: 新增 `MCP_WEB_HOST` 環境變數,支援設定 Web 伺服器主機綁定
- 預設為 `127.0.0.1` 確保安全性
- 可設定為 `0.0.0.0` 允許遠端存取
- 解決 Cursor SSH Remote 等遠端開發環境的存取問題
- 🍎 **macOS 編譯支援增強**: 新增 `.cargo/config.toml` 配置檔案
- 支援 Intel (x86_64) 和 Apple Silicon (aarch64) 架構
- 解決 macOS 上 PyO3 undefined dynamic_lookup 編譯問題
- 遵循 PyO3 官方推薦的最佳實踐
### 🚀 改進功能
- 📝 **工具文檔優化**: 將 LLM 指令移至工具 docstring提升 token 效率
- 🎨 **使用者配置簡化**: 移除複雜的 Cursor 規則配置
- 📊 **AI 工作摘要 Markdown 增強**: 改進 Markdown 渲染效果和相容性
- 🔄 **會話歷史流程優化**: 改進會話保存和管理機制
### 🐛 問題修復
- 🖥️ **桌面應用 MCP 協議修復**: 修正桌面模式下 MCP 協議通訊污染問題
- 📦 **打包流程修復**: 修正多平台桌面應用打包和發布問題
- 🔧 **發布流程優化**: 改進自動化發布工作流程的穩定性
- 🔥 **移除 ESC 快捷鍵**: 移除可能造成意外關閉的 ESC 快捷鍵功能
### 🛠️ 技術改進
- 🏗️ **構建系統增強**: 改進跨平台編譯配置和相依性管理
- 📚 **文檔自動化**: 改進工具自我文檔化,遵循 FastMCP 最佳實踐
- 🔍 **調試功能增強**: 新增更詳細的調試訊息和錯誤處理
### 📋 使用說明
- **SSH 遠端開發**: 在 MCP 配置中設定 `"MCP_WEB_HOST": "0.0.0.0"` 允許遠端存取
- **本地開發**: 保持預設 `"MCP_WEB_HOST": "127.0.0.1"` 確保安全性
- **macOS 開發**: 新的編譯配置將自動生效,無需額外設定
---
## [v2.5.0] - 2025-06-15 - 桌面應用程式與效能優化
### 🌟 版本亮點

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View File

@ -17,7 +17,9 @@ SSH Remote environment limitations:
## Solution
### Step 1: Configure Port (Optional)
### Step 1: Configure Host and Port
You have to set `MCP_WEB_HOST` environment to `0.0.0.0` to allow port forwarding.
MCP Feedback Enhanced uses port **8765** by default, but you can customize the port:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

View File

@ -6,6 +6,7 @@
"timeout": 600,
"env": {
"MCP_DESKTOP_MODE": "true",
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_DEBUG": "false"
},

View File

@ -6,6 +6,7 @@
"timeout": 600,
"env": {
"MCP_DESKTOP_MODE": "false",
"MCP_WEB_HOST": "127.0.0.1",
"MCP_WEB_PORT": "8765",
"MCP_DEBUG": "false"
},

View File

@ -1,54 +0,0 @@
# combinedSummaryContent Markdown 語法顯示功能實作
## 任務概述
為 mcp-feedback-enhanced 專案中的 combinedSummaryContent 區域實現 Markdown 語法顯示功能,將純文字顯示改為支援 Markdown 渲染。
## 技術方案
- **選用庫**marked.js輕量級、高性能
- **引入方式**CDN 直接引用
- **安全處理**:配合 DOMPurify 進行 HTML 清理
- **樣式策略**:保持原生 Markdown 樣式
## 實作計劃
### 階段 1環境準備和依賴引入 ✅
- 修改 feedback.html 模板添加 marked.js CDN 引用
- 驗證庫載入
### 階段 2核心功能實作
- 修改 ui-manager.js 中的 updateAISummaryContent 函數
- 實現 Markdown 解析和渲染
- 添加安全性處理
### 階段 3樣式優化
- 調整 CSS 樣式確保 Markdown 內容正確顯示
- 優化行間距和視覺效果
### 階段 4測試內容和功能驗證
- 建立包含多種 Markdown 語法的測試內容
- 驗證功能正確性
### 階段 5相容性確保
- 確保向後相容性
- 添加錯誤處理機制
## 目標元素
```html
<div id="combinedSummaryContent" class="text-input"
style="min-height: 200px; white-space: pre-wrap !important; cursor: text; padding: 12px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word;"
data-dynamic-content="aiSummary">
```
## 測試內容範例
將包含以下 Markdown 語法:
- 標題(# ## ###
- 粗體和斜體(**bold** *italic*
- 程式碼區塊(```code```
- 列表(- 項目)
- 連結([text](url)
## 預期結果
- 保持現有 CSS 樣式和響應式設計
- 與現有 data-dynamic-content="aiSummary" 機制相容
- 使用原生 Markdown 樣式渲染
- 不影響現有功能

View File

@ -1,6 +1,6 @@
[project]
name = "mcp-feedback-enhanced"
version = "2.5.3"
version = "2.6.0"
description = "Enhanced MCP server for interactive user feedback and command execution in AI-assisted development, featuring dual interface support (Web UI and Desktop Application) with intelligent environment detection and cross-platform compatibility."
readme = "README.md"
requires-python = ">=3.11"

View File

@ -1,4 +1,4 @@
# Release v2.5.3 - Latest Release
# Release v2.6.0 - Latest Release
## 🌟 Key Highlights
@ -23,7 +23,7 @@
uvx mcp-feedback-enhanced@latest
# This specific version / 此特定版本
uvx mcp-feedback-enhanced@v2.5.3
uvx mcp-feedback-enhanced@v2.6.0
```
## 🔗 Links

View File

@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
訊息代碼驗證腳本
驗證後端訊息代碼前端常量和翻譯文件的一致性
確保所有訊息代碼都有對應的定義和翻譯
使用方式
python scripts/validate_message_codes.py
"""
import json
import re
import sys
from pathlib import Path
def extract_backend_codes():
"""從後端 Python 文件中提取所有訊息代碼"""
codes = set()
# 讀取 MessageCodes 類別
message_codes_file = Path(
"src/mcp_feedback_enhanced/web/constants/message_codes.py"
)
if message_codes_file.exists():
content = message_codes_file.read_text(encoding="utf-8")
# 匹配形如 SESSION_FEEDBACK_SUBMITTED = "session.feedbackSubmitted"
pattern = r'([A-Z_]+)\s*=\s*"([^"]+)"'
matches = re.findall(pattern, content)
for constant_name, code in matches:
codes.add(code)
return codes
def extract_frontend_codes():
"""從前端 JavaScript 文件中提取所有訊息代碼"""
codes = set()
# 讀取 message-codes.js
message_codes_js = Path(
"src/mcp_feedback_enhanced/web/static/js/modules/constants/message-codes.js"
)
if message_codes_js.exists():
content = message_codes_js.read_text(encoding="utf-8")
# 匹配形如 FEEDBACK_SUBMITTED: 'session.feedbackSubmitted'
pattern = r'[A-Z_]+:\s*[\'"]([^\'"]+)[\'"]'
matches = re.findall(pattern, content)
codes.update(matches)
# 讀取 utils.js 中的 fallback 訊息
utils_js = Path("src/mcp_feedback_enhanced/web/static/js/modules/utils.js")
if utils_js.exists():
content = utils_js.read_text(encoding="utf-8")
# 匹配 fallbackMessages 物件中的 key
fallback_section = re.search(
r"fallbackMessages\s*=\s*\{([^}]+)\}", content, re.DOTALL
)
if fallback_section:
pattern = r'[\'"]([^\'"]+)[\'"]:\s*[\'"][^\'"]+[\'"]'
matches = re.findall(pattern, fallback_section.group(1))
codes.update(matches)
return codes
def extract_translation_keys(locale="zh-TW"):
"""從翻譯文件中提取所有 key"""
keys = set()
translation_file = Path(
f"src/mcp_feedback_enhanced/web/locales/{locale}/translation.json"
)
if translation_file.exists():
try:
data = json.loads(translation_file.read_text(encoding="utf-8"))
def extract_keys_recursive(obj, prefix=""):
"""遞迴提取所有 key"""
if isinstance(obj, dict):
for key, value in obj.items():
full_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, dict):
extract_keys_recursive(value, full_key)
else:
keys.add(full_key)
extract_keys_recursive(data)
except json.JSONDecodeError as e:
print(f"❌ 無法解析翻譯文件 {translation_file}: {e}")
return keys
def validate_message_codes():
"""執行驗證"""
print("🔍 開始驗證訊息代碼一致性...\n")
# 提取所有代碼
backend_codes = extract_backend_codes()
frontend_codes = extract_frontend_codes()
# 提取所有語言的翻譯 key
locales = ["zh-TW", "en", "zh-CN"]
translation_keys = {}
for locale in locales:
translation_keys[locale] = extract_translation_keys(locale)
# 統計資訊
print("📊 統計資訊:")
print(f" - 後端訊息代碼數量: {len(backend_codes)}")
print(f" - 前端訊息代碼數量: {len(frontend_codes)}")
for locale in locales:
print(f" - {locale} 翻譯 key 數量: {len(translation_keys[locale])}")
print()
# 驗證後端代碼是否都有前端定義
print("🔍 檢查後端代碼是否都有前端定義...")
missing_in_frontend = backend_codes - frontend_codes
if missing_in_frontend:
print("❌ 以下後端代碼在前端沒有定義:")
for code in sorted(missing_in_frontend):
print(f" - {code}")
else:
print("✅ 所有後端代碼都有前端定義")
print()
# 驗證前端代碼是否都有翻譯
print("🔍 檢查前端代碼是否都有翻譯...")
all_frontend_codes = backend_codes | frontend_codes
for locale in locales:
print(f"\n 檢查 {locale} 翻譯:")
missing_translations = set()
for code in all_frontend_codes:
if code not in translation_keys[locale]:
missing_translations.add(code)
if missing_translations:
print(" ❌ 缺少以下翻譯:")
for code in sorted(missing_translations):
print(f" - {code}")
else:
print(" ✅ 所有代碼都有翻譯")
# 檢查是否有多餘的翻譯
print("\n🔍 檢查是否有多餘的翻譯...")
for locale in locales:
# 過濾掉非訊息代碼的 key如 buttons, labels 等)
message_keys = {
k
for k in translation_keys[locale]
if any(
k.startswith(prefix)
for prefix in [
"system.",
"session.",
"settings.",
"error.",
"command.",
"file.",
"prompt.",
"notification.",
]
)
}
extra_translations = message_keys - all_frontend_codes
if extra_translations:
print(f"\n {locale} 有多餘的翻譯:")
for key in sorted(extra_translations):
print(f" - {key}")
print("\n✅ 驗證完成!")
# 返回是否有錯誤
return len(missing_in_frontend) == 0 and all(
len(
[
code
for code in all_frontend_codes
if code not in translation_keys[locale]
]
)
== 0
for locale in locales
)
if __name__ == "__main__":
# 切換到專案根目錄
script_dir = Path(__file__).parent
project_root = script_dir.parent
import os
os.chdir(project_root)
# 執行驗證
success = validate_message_codes()
sys.exit(0 if success else 1)

View File

@ -0,0 +1,11 @@
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]

View File

@ -17,7 +17,7 @@ MCP Interactive Feedback Enhanced
- 重構的模組化架構
"""
__version__ = "2.5.3"
__version__ = "2.6.0"
__author__ = "Minidoracat"
__email__ = "minidora0702@gmail.com"

View File

@ -128,11 +128,16 @@ def test_web_ui_simple():
# 設置測試模式,禁用自動清理避免權限問題
os.environ["MCP_TEST_MODE"] = "true"
os.environ["MCP_WEB_HOST"] = "127.0.0.1"
# 設置更高的端口範圍避免系統保留端口
os.environ["MCP_WEB_PORT"] = "9765"
print("🔧 創建 Web UI 管理器...")
manager = WebUIManager(host="127.0.0.1") # 使用動態端口分配
manager = WebUIManager() # 使用環境變數控制主機和端口
# 顯示最終使用的端口(可能因端口佔用而自動切換)
if manager.port != 9765:
print(f"💡 端口 9765 被佔用,已自動切換到端口 {manager.port}")
print("🔧 創建測試會話...")
with tempfile.TemporaryDirectory() as temp_dir:
@ -218,6 +223,12 @@ def process_feedback(data):
url = f"http://{manager.host}:{manager.port}"
print(f"🌐 服務器運行在: {url}")
# 如果端口有變更,額外提醒
if manager.port != 9765:
print(
f"📌 注意:由於端口 9765 被佔用,服務已切換到端口 {manager.port}"
)
# 嘗試開啟瀏覽器
print("🌐 正在開啟瀏覽器...")
try:
@ -254,6 +265,7 @@ def process_feedback(data):
finally:
# 清理測試環境變數
os.environ.pop("MCP_TEST_MODE", None)
os.environ.pop("MCP_WEB_HOST", None)
os.environ.pop("MCP_WEB_PORT", None)

View File

@ -20,8 +20,6 @@ try:
from mcp_feedback_enhanced.web.main import WebUIManager, get_web_ui_manager
except ImportError as e:
# 在這裡無法使用 debug_log因為導入失敗
import sys
sys.stderr.write(f"無法導入 MCP Feedback Enhanced 模組: {e}\n")
sys.exit(1)
@ -223,7 +221,8 @@ class DesktopApp:
# Windows 下隱藏控制台視窗
creation_flags = 0
if os.name == "nt":
creation_flags = subprocess.CREATE_NO_WINDOW
# CREATE_NO_WINDOW 只在 Windows 上存在
creation_flags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
self.app_handle = subprocess.Popen(
[str(tauri_exe)],
@ -329,7 +328,7 @@ def run_desktop_app():
loop.close()
except Exception as e:
debug_log(f"桌面應用程式運行失敗: {e}")
sys.stderr.write(f"桌面應用程式運行失敗: {e}\n")
sys.exit(1)

View File

@ -30,8 +30,8 @@ class I18nManager:
def __init__(self):
self._current_language = None
self._translations = {}
self._supported_languages = ["zh-TW", "en", "zh-CN"]
self._fallback_language = "en"
self._supported_languages = ["zh-CN", "zh-TW", "en"]
self._fallback_language = "zh-TW"
self._config_file = self._get_config_file_path()
self._locales_dir = Path(__file__).parent / "web" / "locales"

View File

@ -434,28 +434,22 @@ async def interactive_feedback(
] = "我已完成了您請求的任務。",
timeout: Annotated[int, Field(description="等待用戶回饋的超時時間(秒)")] = 600,
) -> list:
"""
收集用戶的互動回饋支援文字和圖片
"""Interactive feedback collection tool for LLM agents.
此工具使用 Web UI 介面收集用戶回饋支援智能環境檢測
用戶可以
1. 執行命令來驗證結果
2. 提供文字回饋
3. 上傳圖片作為回饋
4. 查看 AI 的工作摘要
調試模式
- 設置環境變數 MCP_DEBUG=true 可啟用詳細調試輸出
- 生產環境建議關閉調試模式以避免輸出干擾
USAGE RULES:
1. During any process, task, or conversation, whether asking, replying, or completing phased tasks, you must call this tool to ask for feedback.
2. Unless receiving termination instructions, all steps must repeatedly call this tool.
3. Whenever user feedback is received, if the feedback content is not empty, you must call this tool again and adjust behavior based on the feedback content.
4. Only when the user explicitly indicates "end" or "no more interaction needed" can you stop calling this tool, and the process is considered complete.
5. You should summarize what have done, and provide project directory through args to let user know what you have done to provide feedback for next step.
Args:
project_directory: 專案目錄路徑
summary: AI 工作完成的摘要說明
timeout: 等待用戶回饋的超時時間預設為 600 10 分鐘
project_directory: Project directory path for context
summary: Summary of AI work completed for user review
timeout: Timeout in seconds for waiting user feedback (default: 600 seconds)
Returns:
List: 包含 TextContent MCPImage 對象的列表
list: List containing TextContent and MCPImage objects representing user feedback
"""
# 環境偵測
is_remote = is_remote_environment()
@ -599,7 +593,22 @@ def get_system_info() -> str:
# ===== 主程式入口 =====
def main():
"""主要入口點,用於套件執行"""
"""主要入口點,用於套件執行
收集用戶的互動回饋支援文字和圖片
此工具使用 Web UI 介面收集用戶回饋支援智能環境檢測
用戶可以
1. 執行命令來驗證結果
2. 提供文字回饋
3. 上傳圖片作為回饋
4. 查看 AI 的工作摘要
調試模式
- 設置環境變數 MCP_DEBUG=true 可啟用詳細調試輸出
- 生產環境建議關閉調試模式以避免輸出干擾
"""
# 檢查是否啟用調試模式
debug_enabled = os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on")

View File

@ -0,0 +1,8 @@
"""
Web 常量模組
"""
from .message_codes import MessageCodes, get_message_code
__all__ = ["MessageCodes", "get_message_code"]

View File

@ -0,0 +1,173 @@
"""
統一的訊息代碼定義
這個模組定義了所有後端使用的訊息代碼常量
前端會根據這些代碼顯示對應的本地化訊息
使用方式
from ..constants import MessageCodes, get_message_code
# 使用常量
code = MessageCodes.SESSION_FEEDBACK_SUBMITTED
# 或使用輔助函數
code = get_message_code("SESSION_FEEDBACK_SUBMITTED")
"""
class MessageCodes:
"""訊息代碼常量類"""
# ========== 系統相關 ==========
SYSTEM_CONNECTION_ESTABLISHED = "system.connectionEstablished"
SYSTEM_CONNECTION_LOST = "system.connectionLost"
SYSTEM_CONNECTION_RECONNECTING = "system.connectionReconnecting"
SYSTEM_CONNECTION_RECONNECTED = "system.connectionReconnected"
SYSTEM_CONNECTION_FAILED = "system.connectionFailed"
SYSTEM_WEBSOCKET_ERROR = "system.websocketError"
SYSTEM_WEBSOCKET_READY = "system.websocketReady"
SYSTEM_MEMORY_PRESSURE = "system.memoryPressure"
SYSTEM_SHUTDOWN = "system.shutdown"
SYSTEM_PROCESS_KILLED = "system.processKilled"
SYSTEM_HEARTBEAT_STOPPED = "system.heartbeatStopped"
# ========== 會話相關 ==========
SESSION_NO_ACTIVE = "session.noActiveSession"
SESSION_CREATED = "session.created"
SESSION_UPDATED = "session.updated"
SESSION_EXPIRED = "session.expired"
SESSION_TIMEOUT = "session.timeout"
SESSION_CLEANED = "session.cleaned"
SESSION_FEEDBACK_SUBMITTED = "session.feedbackSubmitted"
SESSION_USER_MESSAGE_RECORDED = "session.userMessageRecorded"
SESSION_HISTORY_SAVED = "session.historySaved"
SESSION_HISTORY_LOADED = "session.historyLoaded"
SESSION_MANUAL_CLEANUP = "session.manualCleanup"
SESSION_ERROR_CLEANUP = "session.errorCleanup"
# ========== 設定相關 ==========
SETTINGS_SAVED = "settingsAPI.saved"
SETTINGS_LOADED = "settingsAPI.loaded"
SETTINGS_CLEARED = "settingsAPI.cleared"
SETTINGS_SAVE_FAILED = "settingsAPI.saveFailed"
SETTINGS_LOAD_FAILED = "settingsAPI.loadFailed"
SETTINGS_CLEAR_FAILED = "settingsAPI.clearFailed"
SETTINGS_SET_FAILED = "settingsAPI.setFailed"
SETTINGS_INVALID_VALUE = "settingsAPI.invalidValue"
SETTINGS_LOG_LEVEL_UPDATED = "settingsAPI.logLevelUpdated"
SETTINGS_INVALID_LOG_LEVEL = "settingsAPI.invalidLogLevel"
# ========== 命令執行相關 ==========
COMMAND_EXECUTING = "commandStatus.executing"
COMMAND_COMPLETED = "commandStatus.completed"
COMMAND_FAILED = "commandStatus.failed"
COMMAND_INVALID = "commandStatus.invalid"
COMMAND_OUTPUT_RECEIVED = "commandStatus.outputReceived"
COMMAND_ERROR = "commandStatus.error"
# ========== 錯誤相關 ==========
ERROR_GENERIC = "error.generic"
ERROR_NETWORK = "error.network"
ERROR_SERVER = "error.server"
ERROR_TIMEOUT = "error.timeout"
ERROR_INVALID_INPUT = "error.invalidInput"
ERROR_OPERATION_FAILED = "error.operationFailed"
ERROR_USER_MESSAGE_FAILED = "error.userMessageFailed"
ERROR_GET_SESSIONS_FAILED = "error.getSessionsFailed"
ERROR_GET_LOG_LEVEL_FAILED = "error.getLogLevelFailed"
ERROR_RESOURCE_CLEANUP = "error.resourceCleanup"
ERROR_PROCESSING = "error.processing"
# ========== 檔案相關 ==========
FILE_UPLOAD_SUCCESS = "file.uploadSuccess"
FILE_UPLOAD_FAILED = "file.uploadFailed"
FILE_SIZE_TOO_LARGE = "file.sizeTooLarge"
FILE_TYPE_NOT_SUPPORTED = "file.typeNotSupported"
FILE_PROCESSING = "file.processing"
FILE_REMOVED = "file.removed"
# ========== 提示詞相關 ==========
PROMPT_SAVED = "prompt.saved"
PROMPT_DELETED = "prompt.deleted"
PROMPT_APPLIED = "prompt.applied"
PROMPT_IMPORT_SUCCESS = "prompt.importSuccess"
PROMPT_IMPORT_FAILED = "prompt.importFailed"
PROMPT_EXPORT_SUCCESS = "prompt.exportSuccess"
PROMPT_VALIDATION_FAILED = "prompt.validationFailed"
# 向後兼容的映射表(從舊的 key 到新的常量名稱)
LEGACY_KEY_MAPPING = {
# feedback_session.py 的舊 key
"FEEDBACK_SUBMITTED": "SESSION_FEEDBACK_SUBMITTED",
"SESSION_CLEANUP": "SESSION_CLEANED",
"TIMEOUT_CLEANUP": "SESSION_TIMEOUT",
"EXPIRED_CLEANUP": "SESSION_EXPIRED",
"MEMORY_PRESSURE_CLEANUP": "SYSTEM_MEMORY_PRESSURE",
"MANUAL_CLEANUP": "SESSION_MANUAL_CLEANUP",
"ERROR_CLEANUP": "SESSION_ERROR_CLEANUP",
"SHUTDOWN_CLEANUP": "SYSTEM_SHUTDOWN",
"COMMAND_EXECUTING": "COMMAND_EXECUTING",
"COMMAND_COMPLETED": "COMMAND_COMPLETED",
"COMMAND_FAILED": "COMMAND_FAILED",
"COMMAND_INVALID": "COMMAND_INVALID",
"COMMAND_ERROR": "COMMAND_ERROR",
"PROCESS_KILLED": "SYSTEM_PROCESS_KILLED",
"RESOURCE_CLEANUP_ERROR": "ERROR_RESOURCE_CLEANUP",
"HEARTBEAT_STOPPED": "SYSTEM_HEARTBEAT_STOPPED",
"PROCESSING_ERROR": "ERROR_PROCESSING",
"WEBSOCKET_READY": "SYSTEM_WEBSOCKET_READY",
# main_routes.py 的舊 key
"no_active_session": "SESSION_NO_ACTIVE",
"websocket_connected": "SYSTEM_CONNECTION_ESTABLISHED",
"new_session_created": "SESSION_CREATED",
"user_message_recorded": "SESSION_USER_MESSAGE_RECORDED",
"add_user_message_failed": "ERROR_USER_MESSAGE_FAILED",
"settings_saved": "SETTINGS_SAVED",
"save_failed": "SETTINGS_SAVE_FAILED",
"load_failed": "SETTINGS_LOAD_FAILED",
"settings_cleared": "SETTINGS_CLEARED",
"clear_failed": "SETTINGS_CLEAR_FAILED",
"session_history_saved": "SESSION_HISTORY_SAVED",
"get_sessions_failed": "ERROR_GET_SESSIONS_FAILED",
"get_log_level_failed": "ERROR_GET_LOG_LEVEL_FAILED",
"invalid_log_level": "SETTINGS_INVALID_LOG_LEVEL",
"log_level_updated": "SETTINGS_LOG_LEVEL_UPDATED",
"set_failed": "SETTINGS_SET_FAILED",
}
def get_message_code(key: str) -> str:
"""
獲取訊息代碼
支援三種輸入方式
1. 直接使用常量名稱get_message_code("SESSION_FEEDBACK_SUBMITTED")
2. 使用舊的 key向後兼容get_message_code("FEEDBACK_SUBMITTED")
3. 使用小寫的舊 key向後兼容get_message_code("feedback_submitted")
Args:
key: 訊息 key 或常量名稱
Returns:
訊息代碼字串例如"session.feedbackSubmitted"
"""
# 嘗試直接從 MessageCodes 獲取
if hasattr(MessageCodes, key):
return str(getattr(MessageCodes, key))
# 嘗試從映射表獲取(支援大寫和小寫)
upper_key = key.upper()
if upper_key in LEGACY_KEY_MAPPING:
constant_name = LEGACY_KEY_MAPPING[upper_key]
if hasattr(MessageCodes, constant_name):
return str(getattr(MessageCodes, constant_name))
# 如果是小寫的 key也嘗試映射
if key in LEGACY_KEY_MAPPING:
constant_name = LEGACY_KEY_MAPPING[key]
if hasattr(MessageCodes, constant_name):
return str(getattr(MessageCodes, constant_name))
# 如果都找不到,返回一個預設格式
return f"unknown.{key}"

View File

@ -12,7 +12,6 @@
"updateFailed": "Failed to update content, please manually refresh the page to view new AI work summary"
},
"tabs": {
"feedback": "💬 Feedback",
"summary": "📋 AI Summary",
"commands": "⚡ Commands",
"command": "⚡ Commands",
@ -48,7 +47,11 @@
"processingFeedback": "Processing, please wait",
"connectingMessage": "WebSocket connecting, feedback will be submitted automatically when connection is ready...",
"invalidState": "Current state does not allow submission",
"sendFailed": "Send failed, please retry"
"sendFailed": "Send failed, please retry",
"noContent": "No content to copy",
"copySuccess": "Content copied to clipboard",
"copyFailed": "Failed to copy",
"provideTextOrImage": "Please provide feedback text or upload an image"
},
"summary": {
"title": "📋 AI Work Summary",
@ -88,18 +91,34 @@
"running": "Running...",
"completed": "Completed",
"error": "Execution Error",
"history": "Command History"
"history": "Command History",
"autoCommand": {
"title": "🤖 Auto Command Settings",
"description": "Configure commands to execute automatically at specific times",
"enabled": "Enable Auto Commands",
"onNewSession": "Execute on New Session",
"onNewSessionPlaceholder": "e.g., pwd or git status",
"onFeedbackSubmit": "Execute on Feedback Submit",
"onFeedbackSubmitPlaceholder": "e.g., echo 'Feedback submitted'",
"testOnNewSession": "Test New Session Command",
"testOnFeedbackSubmit": "Test Feedback Submit Command",
"help": "These commands will execute automatically at the corresponding times. Leave empty to skip execution."
},
"autoSettings": {
"exampleNewSession": "💡 Example: pwd, git status, ls -la",
"exampleFeedback": "💡 Example: date, echo \"Done\", git log -1"
}
},
"combined": {
"summaryTitle": "📋 AI Work Summary",
"feedbackTitle": "💬 Provide Feedback"
},
"settings": {
"settingsUI": {
"title": "⚙️ Settings",
"language": "Language",
"language": "🌍 Language",
"currentLanguage": "Current Language",
"languageDesc": "Select interface display language",
"interface": "Interface Settings",
"interface": "🎨 Interface Settings",
"layoutMode": "Interface Layout Mode",
"layoutModeDesc": "Select how AI summary and feedback input are displayed",
"combinedVertical": "Vertical Layout",
@ -110,7 +129,7 @@
"autoCloseDesc": "Automatically close page after submitting feedback",
"theme": "Theme",
"notifications": "Notifications",
"advanced": "Advanced Settings",
"advanced": "🔧 Advanced Settings",
"save": "Save Settings",
"reset": "Reset Settings",
"resetDesc": "Clear all saved settings and restore to default state",
@ -119,7 +138,13 @@
"resetError": "Error occurred while resetting settings",
"timeout": "Connection Timeout (seconds)",
"autorefresh": "Auto Refresh",
"debug": "Debug Mode"
"debug": "Debug Mode",
"sessionTimeoutTitle": "⏱️ Session Timeout Settings",
"sessionTimeoutEnable": "Enable Session Timeout",
"sessionTimeoutEnableDesc": "When enabled, the session will automatically close after the specified time",
"sessionTimeoutDuration": "Timeout Duration (seconds)",
"sessionTimeoutDurationDesc": "Set session timeout duration, range: 300-86400 seconds (5 minutes - 24 hours)",
"sessionTimeoutSeconds": "seconds"
},
"languages": {
"zh-TW": "繁體中文",
@ -157,6 +182,10 @@
"submitted": {
"title": "Submitted",
"message": "Waiting for next MCP call"
},
"completed": {
"title": "Completed",
"message": "Session completed"
}
},
"notifications": {
@ -189,11 +218,12 @@
"upload": "Upload",
"download": "Download"
},
"session": {
"sessionTimeout": {
"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..."
"closing": "Closing...",
"label": "Session Timeout:"
},
"autoRefresh": {
"enable": "Auto Detect",
@ -235,22 +265,30 @@
"summary": "Summary",
"noSummary": "No summary available",
"unknown": "Unknown"
}
},
"copySessionContent": "Copy Session Content",
"copyUserContent": "Copy User Content",
"exportSession": "Export This Session"
},
"sessionHistory": {
"management": {
"title": "Session History Management",
"title": "📚 Session History Management",
"retentionPeriod": "Retention Period",
"retentionHours": "hours",
"export": "Export",
"clear": "Clear",
"exportAll": "Export All",
"exportAllTitle": "Export all session history to file",
"exportSingle": "Export This Session",
"confirmClear": "Are you sure you want to clear all session history?",
"exportSuccess": "Session history exported successfully",
"clearSuccess": "Session history cleared successfully",
"clearTitle": "Clear all session history records",
"description": "Manage locally stored session history records, including retention period settings and data export functionality",
"exportDescription": "Export or clear locally stored session history records"
"exportDescription": "Export or clear locally stored session history records",
"exportFailed": "Export failed: {error}",
"clearFailed": "Clear failed: {error}",
"clearFailedGeneric": "Clear failed"
},
"retention": {
"24hours": "24 hours",
@ -282,8 +320,17 @@
"imageCount": "Image Count",
"timestamp": "Timestamp",
"clearAll": "Clear Message Records",
"clearAllTitle": "Clear all user message records from all sessions",
"confirmClearAll": "Are you sure you want to clear all user message records from all sessions? This action cannot be undone.",
"clearSuccess": "User message records cleared successfully"
},
"noActiveSession": "No active session data",
"sessionNotFound": "Session data not found",
"currentSession": {
"noData": "No current session data",
"noUserMessages": "Current session has no user message records",
"copyFailed": "Copy failed, please try again",
"dataManagerNotInit": "Data manager not initialized"
}
},
"connectionMonitor": {
@ -315,7 +362,11 @@
"latencyMs": "Latency",
"sessions": "Sessions",
"reconnects": "Reconnects"
}
},
"unknown": "Unknown"
},
"stats": {
"detailedStats": "Detailed Statistics"
},
"dynamic": {
@ -324,7 +375,7 @@
},
"prompts": {
"management": {
"title": "Prompt Templates Management",
"title": "📝 Prompt Templates Management",
"description": "Manage your frequently used prompt templates for quick selection during feedback input",
"addNew": "Add New Prompt",
"edit": "Edit",
@ -374,6 +425,7 @@
},
"about": {
"title": " About",
"description": "A powerful MCP server that provides human-in-the-loop feedback functionality for AI-assisted development tools. Features a Web UI interface with rich capabilities including image upload, command execution, and multi-language support.",
"appInfo": "Application Information",
"version": "Version",
"projectLinks": "Project Links",
@ -388,7 +440,7 @@
},
"images": {
"settings": {
"title": "Image Settings",
"title": "🖼️ Image Settings",
"sizeLimit": "Image Size Limit",
"sizeLimitDesc": "Set the maximum file size limit for uploaded images",
"sizeLimitOptions": {
@ -407,7 +459,7 @@
"sizeLimitExceededAdvice": "Consider compressing the image with editing software before uploading, or adjust the image size limit settings."
},
"autoSubmit": {
"title": "Auto Timed Submit",
"title": "Auto Timed Submit",
"enable": "Enable Auto Submit",
"enableDesc": "When enabled, automatically submit selected prompt content after specified time",
"timeout": "Countdown Time (seconds)",
@ -420,11 +472,15 @@
"enabled": "Enabled",
"disabled": "Disabled",
"executing": "Executing auto submit...",
"countdownLabel": "Submit Countdown"
"countdownLabel": "Submit Countdown",
"pauseCountdown": "Pause Countdown",
"resumeCountdown": "Resume Countdown",
"paused": "Auto submit paused",
"autoCommitNoPrompt": "Please select a prompt as auto-submit content first"
},
"audio": {
"notification": {
"title": "Audio Notification Settings",
"title": "🔊 Audio Notification Settings",
"description": "Configure audio notifications for session updates",
"enabled": "Enable Audio Notifications",
"enabledDesc": "Play audio notifications when there are new session updates",
@ -457,5 +513,132 @@
"testPlaying": "Playing test audio",
"audioNotFound": "Selected audio not found"
}
},
"notification": {
"title": "Browser Notifications",
"settingLabel": "Browser Notifications",
"description": "Get notified when new sessions are created (only when in background)",
"enabled": "Notifications enabled ✅",
"disabled": "Notifications disabled",
"permissionRequired": "Notification permission required to enable this feature",
"permissionDenied": "Browser has blocked notifications, please allow in browser settings",
"permissionGranted": "Granted",
"permissionDeniedStatus": "Denied (please modify in browser settings)",
"permissionDefault": "Not set",
"notSupported": "Your browser does not support notifications",
"enableFailed": "Failed to enable notifications",
"test": "Send test notification",
"testTitle": "Test Notification",
"testDescription": "Send a test notification to confirm it's working",
"autoplayBlocked": "Browser blocked autoplay, please click the page to enable audio notifications",
"triggerTitle": "Notification Trigger Context",
"triggerDescription": "Choose when to receive notifications",
"triggerModeUpdated": "Notification trigger mode updated",
"trigger": {
"focusLost": "When window loses focus (switching to other apps)",
"tabSwitch": "When switching to other browser tabs",
"background": "When window is minimized or hidden",
"always": "Always notify (including foreground)"
},
"browser": {
"title": "MCP Feedback - New Session",
"ready": "Ready",
"unknownProject": "Unknown Project",
"testTitle": "Test Notification",
"testBody": "This is a test notification, it will close automatically in 5 seconds",
"notSupported": "Your browser does not support notifications",
"permissionRequired": "Please grant notification permission first",
"criticalTitle": "MCP Feedback - Warning"
}
},
"system": {
"connectionEstablished": "WebSocket connection established",
"connectionLost": "WebSocket connection lost",
"connectionReconnecting": "Reconnecting...",
"connectionReconnected": "Reconnected",
"connectionFailed": "Connection failed",
"websocketError": "WebSocket error",
"memoryPressure": "Memory pressure cleanup",
"shutdown": "System shutdown",
"processKilled": "Process killed",
"heartbeatStopped": "Heartbeat stopped",
"websocketReady": "WebSocket ready"
},
"session": {
"noActiveSession": "No active session",
"created": "New MCP session created, page will refresh automatically",
"updated": "Session updated",
"expired": "Session expired",
"timeout": "Session timed out",
"cleaned": "Session cleaned",
"feedbackSubmitted": "Feedback submitted successfully",
"userMessageRecorded": "User message recorded",
"historySaved": "Session history saved ({{count}} sessions)",
"historyLoaded": "Session history loaded"
},
"settingsAPI": {
"saved": "Settings saved",
"loaded": "Settings loaded",
"cleared": "Settings cleared",
"saveFailed": "Save failed",
"loadFailed": "Load failed",
"clearFailed": "Clear failed",
"setFailed": "Set failed",
"invalidValue": "Invalid setting value",
"logLevelUpdated": "Log level updated",
"invalidLogLevel": "Invalid log level, must be one of DEBUG, INFO, WARN, ERROR"
},
"file": {
"uploadSuccess": "File uploaded successfully",
"uploadFailed": "File upload failed",
"sizeTooLarge": "File size exceeds limit",
"typeNotSupported": "File type not supported",
"processing": "Processing file...",
"removed": "File removed"
},
"prompt": {
"saved": "Prompt saved",
"deleted": "Prompt deleted",
"applied": "Prompt applied: {{name}}",
"importSuccess": "Prompts imported successfully",
"importFailed": "Prompts import failed",
"exportSuccess": "Prompts exported successfully",
"validationFailed": "Prompt validation failed"
},
"error": {
"generic": "An error occurred: {{error}}",
"network": "Network error",
"server": "Server error",
"timeout": "Operation timed out",
"invalidInput": "Invalid input",
"operationFailed": "Operation failed",
"userMessageFailed": "Failed to add user message",
"getSessionsFailed": "Failed to get sessions",
"getLogLevelFailed": "Failed to get log level",
"command": "Command execution error",
"resourceCleanup": "Resource cleanup error",
"processing": "Processing error"
},
"commandStatus": {
"executing": "Executing command...",
"completed": "Command completed",
"failed": "Command failed",
"outputReceived": "Output received",
"invalid": "Invalid command",
"error": "Command execution error"
},
"utils": {
"copySuccess": "Copied to clipboard",
"copyError": "Copy failed"
},
"fileUpload": {
"fileSizeExceeded": "Image size exceeds limit ({limit}): {filename}",
"maxFilesExceeded": "Maximum {maxFiles} files can be uploaded",
"processingFailed": "File processing failed, please retry"
},
"aria": {
"toggleAutoSubmit": "Toggle auto submit",
"toggleNotification": "Toggle notification",
"toggleAudioNotification": "Toggle audio notification"
}
}

View File

@ -12,7 +12,6 @@
"updateFailed": "更新内容失败,请手动刷新页面以查看新的 AI 工作摘要"
},
"tabs": {
"feedback": "💬 反馈",
"summary": "📋 AI 总结",
"commands": "⚡ 命令",
"command": "⚡ 命令",
@ -48,7 +47,11 @@
"processingFeedback": "正在处理中,请稍候",
"connectingMessage": "WebSocket 连接中,反馈将在连接就绪后自动提交...",
"invalidState": "当前状态不允许提交",
"sendFailed": "发送失败,请重试"
"sendFailed": "发送失败,请重试",
"noContent": "没有可复制的内容",
"copySuccess": "内容已复制到剪贴板",
"copyFailed": "复制失败",
"provideTextOrImage": "请提供反馈文字或上传图片"
},
"summary": {
"title": "📋 AI 工作摘要",
@ -88,18 +91,34 @@
"running": "执行中...",
"completed": "执行完成",
"error": "执行错误",
"history": "命令历史"
"history": "命令历史",
"autoCommand": {
"title": "🤖 自动执行命令设置",
"description": "设置在特定时机自动执行的命令",
"enabled": "启用自动执行命令",
"onNewSession": "新会话建立时执行",
"onNewSessionPlaceholder": "例如pwd 或 git status",
"onFeedbackSubmit": "提交反馈后执行",
"onFeedbackSubmitPlaceholder": "例如echo '反馈已提交'",
"testOnNewSession": "测试新会话命令",
"testOnFeedbackSubmit": "测试反馈提交命令",
"help": "这些命令会在对应的时机自动执行。留空表示不执行任何命令。"
},
"autoSettings": {
"exampleNewSession": "💡 示例pwd, git status, ls -la",
"exampleFeedback": "💡 示例date, echo \"Done\", git log -1"
}
},
"combined": {
"summaryTitle": "📋 AI 工作摘要",
"feedbackTitle": "💬 提供反馈"
},
"settings": {
"settingsUI": {
"title": "⚙️ 设定",
"language": "语言",
"language": "🌍 语言",
"currentLanguage": "当前语言",
"languageDesc": "选择界面显示语言",
"interface": "界面设定",
"interface": "🎨 界面设定",
"layoutMode": "界面布局模式",
"layoutModeDesc": "选择 AI 摘要和反馈输入的显示方式",
"combinedVertical": "垂直布局",
@ -110,7 +129,7 @@
"autoCloseDesc": "提交回馈后自动关闭页面",
"theme": "主题",
"notifications": "通知",
"advanced": "进阶设定",
"advanced": "🔧 进阶设定",
"save": "储存设定",
"reset": "重置设定",
"resetDesc": "清除所有已保存的设定,恢复到预设状态",
@ -119,7 +138,14 @@
"resetError": "重置设定时发生错误",
"timeout": "连线逾时 (秒)",
"autorefresh": "自动重新整理",
"debug": "除错模式"
"debug": "除错模式",
"autoCommitNoPrompt": "请先选择一个提示词作为自动提交内容",
"sessionTimeoutTitle": "⏱️ 会话超时设置",
"sessionTimeoutEnable": "启用会话超时",
"sessionTimeoutEnableDesc": "启用后,会话将在指定时间后自动关闭",
"sessionTimeoutDuration": "超时时间(秒)",
"sessionTimeoutDurationDesc": "设置会话超时时间范围300-86400 秒5分钟-24小时",
"sessionTimeoutSeconds": "秒"
},
"languages": {
"zh-TW": "繁體中文",
@ -157,6 +183,10 @@
"submitted": {
"title": "反馈已提交",
"message": "等待下次 MCP 调用"
},
"completed": {
"title": "已完成",
"message": "会话已完成"
}
},
"notifications": {
@ -189,11 +219,12 @@
"upload": "上传",
"download": "下载"
},
"session": {
"sessionTimeout": {
"timeout": "⏰ 会话已超时,界面将自动关闭",
"timeoutWarning": "会话即将超时",
"timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。",
"closing": "正在关闭..."
"closing": "正在关闭...",
"label": "会话超时:"
},
"autoRefresh": {
"enable": "自动检测",
@ -235,22 +266,30 @@
"summary": "总结",
"noSummary": "暂无总结",
"unknown": "未知"
}
},
"copySessionContent": "复制会话内容",
"copyUserContent": "复制用户内容",
"exportSession": "导出此会话"
},
"sessionHistory": {
"management": {
"title": "会话历史管理",
"title": "📚 会话历史管理",
"retentionPeriod": "保存期限",
"retentionHours": "小时",
"export": "导出",
"clear": "清空",
"exportAll": "导出全部",
"exportAllTitle": "导出所有会话历史到文件",
"exportSingle": "导出此会话",
"confirmClear": "确定要清空所有会话历史吗?",
"exportSuccess": "会话历史已导出",
"clearSuccess": "会话历史已清空",
"clearTitle": "清空所有会话历史记录",
"description": "管理本地存储的会话历史记录,包括保存期限设定和数据导出功能",
"exportDescription": "导出或清空本地存储的会话历史记录"
"exportDescription": "导出或清空本地存储的会话历史记录",
"exportFailed": "导出失败: {error}",
"clearFailed": "清空失败: {error}",
"clearFailedGeneric": "清空失败"
},
"retention": {
"24hours": "24 小时",
@ -282,8 +321,17 @@
"imageCount": "图片数量",
"timestamp": "时间戳",
"clearAll": "清空消息记录",
"clearAllTitle": "清空所有会话的用户消息记录",
"confirmClearAll": "确定要清空所有会话的用户消息记录吗?此操作无法撤销。",
"clearSuccess": "用户消息记录已清空"
},
"noActiveSession": "目前没有活跃的会话数据",
"sessionNotFound": "找不到会话资料",
"currentSession": {
"noData": "没有当前会话数据",
"noUserMessages": "当前会话没有用户消息记录",
"copyFailed": "复制失败,请重试",
"dataManagerNotInit": "数据管理器未初始化"
}
},
"connectionMonitor": {
@ -324,7 +372,7 @@
},
"prompts": {
"management": {
"title": "常用提示词管理",
"title": "📝 常用提示词管理",
"description": "管理您的常用提示词模板,可在反馈输入时快速选用",
"addNew": "新增提示词",
"edit": "编辑",
@ -374,6 +422,7 @@
},
"about": {
"title": " 关于",
"description": "一个强大的 MCP 服务器,为 AI 辅助开发工具提供人在回路的互动反馈功能。支持 Web UI 界面,并具备图片上传、命令执行、多语言等丰富功能。",
"appInfo": "应用程序信息",
"version": "版本",
"projectLinks": "项目链接",
@ -388,7 +437,7 @@
},
"images": {
"settings": {
"title": "图片设置",
"title": "🖼️ 图片设置",
"sizeLimit": "图片大小限制",
"sizeLimitDesc": "设定上传图片的最大文件大小限制",
"sizeLimitOptions": {
@ -407,7 +456,7 @@
"sizeLimitExceededAdvice": "建议使用图片编辑软件压缩后再上传,或调整图片大小限制设置。"
},
"autoSubmit": {
"title": "自动定时提交",
"title": "自动定时提交",
"enable": "启用自动提交",
"enableDesc": "启用后将在指定时间自动提交选定的提示词内容",
"timeout": "倒数时间(秒)",
@ -420,11 +469,14 @@
"enabled": "已启用",
"disabled": "已停用",
"executing": "正在执行自动提交...",
"countdownLabel": "提交倒数"
"countdownLabel": "提交倒数",
"pauseCountdown": "暂停倒数",
"resumeCountdown": "恢复倒数",
"paused": "自动提交已暂停"
},
"audio": {
"notification": {
"title": "音效通知设定",
"title": "🔊 音效通知设定",
"description": "设定会话更新时的音效通知",
"enabled": "启用音效通知",
"enabledDesc": "启用后将在有新会话更新时播放音效通知",
@ -457,5 +509,122 @@
"testPlaying": "正在播放测试音效",
"audioNotFound": "找不到选择的音效"
}
},
"stats": {
"detailedStats": "详细统计信息"
},
"notification": {
"title": "浏览器通知",
"settingLabel": "浏览器通知",
"description": "新会话建立时通知(仅在后台运行时)",
"enabled": "通知已启用 ✅",
"disabled": "通知已关闭",
"permissionRequired": "需要通知权限才能启用此功能",
"permissionDenied": "浏览器已封锁通知,请在浏览器设置中允许",
"permissionGranted": "已授权",
"permissionDeniedStatus": "已拒绝(请在浏览器设置中修改)",
"permissionDefault": "尚未设置",
"notSupported": "您的浏览器不支持通知功能",
"enableFailed": "启用通知失败",
"test": "发送测试通知",
"testTitle": "测试通知",
"testDescription": "发送测试通知以确认功能正常",
"autoplayBlocked": "浏览器阻止音效自动播放,请点击页面以启用音效通知",
"triggerTitle": "通知触发场景",
"triggerDescription": "选择何时接收通知",
"triggerModeUpdated": "通知触发模式已更新",
"trigger": {
"focusLost": "窗口失去焦点时(切换到其他应用程序)",
"tabSwitch": "切换到其他标签页时",
"background": "窗口最小化或隐藏时",
"always": "总是通知(包括前景)"
},
"browser": {
"title": "MCP Feedback - 新会话",
"ready": "准备就绪",
"unknownProject": "未知项目",
"testTitle": "测试通知",
"testBody": "这是一个测试通知5秒后将自动关闭",
"notSupported": "您的浏览器不支持通知功能",
"permissionRequired": "请先授权通知权限",
"criticalTitle": "MCP Feedback - 警告"
}
},
"system": {
"connectionEstablished": "WebSocket 连接已建立",
"connectionLost": "WebSocket 连接已断开",
"connectionReconnecting": "正在重新连接...",
"connectionReconnected": "已重新连接",
"connectionFailed": "连接失败",
"websocketError": "WebSocket 错误"
},
"session": {
"noActiveSession": "没有活跃会话",
"created": "新的 MCP 会话已创建,页面将自动刷新",
"updated": "会话已更新",
"expired": "会话已过期",
"timeout": "会话已超时",
"cleaned": "会话已清理",
"feedbackSubmitted": "反馈已成功提交",
"userMessageRecorded": "用户消息已记录",
"historySaved": "会话历史已保存({{count}} 个会话)",
"historyLoaded": "会话历史已载入"
},
"settingsAPI": {
"saved": "设置已保存",
"loaded": "设置已载入",
"cleared": "设置已清除",
"saveFailed": "保存失败",
"loadFailed": "加载失败",
"clearFailed": "清除失败",
"invalidValue": "无效的设置值",
"logLevelUpdated": "日志等级已更新",
"invalidLogLevel": "无效的日志等级,必须是 DEBUG, INFO, WARN, ERROR 之一"
},
"file": {
"uploadSuccess": "文件上传成功",
"uploadFailed": "文件上传失败",
"sizeTooLarge": "文件大小超过限制",
"typeNotSupported": "不支持的文件类型",
"processing": "正在处理文件...",
"removed": "文件已移除"
},
"prompt": {
"saved": "提示词已保存",
"deleted": "提示词已删除",
"applied": "已套用提示词:{{name}}",
"importSuccess": "提示词导入成功",
"importFailed": "提示词导入失败",
"exportSuccess": "提示词导出成功",
"validationFailed": "提示词验证失败"
},
"error": {
"generic": "发生错误:{{error}}",
"network": "网络错误",
"server": "服务器错误",
"timeout": "操作超时",
"invalidInput": "输入无效",
"operationFailed": "操作失败"
},
"commandStatus": {
"executing": "正在执行命令...",
"completed": "命令执行完成",
"failed": "命令执行失败",
"outputReceived": "已接收输出",
"invalid": "无效的命令"
},
"utils": {
"copySuccess": "已复制到剪贴板",
"copyError": "复制失败"
},
"fileUpload": {
"fileSizeExceeded": "图片大小超过限制 ({limit}): {filename}",
"maxFilesExceeded": "最多只能上传 {maxFiles} 个文件",
"processingFailed": "文件处理失败,请重试"
},
"aria": {
"toggleAutoSubmit": "切换自动提交",
"toggleNotification": "切换通知",
"toggleAudioNotification": "切换音效通知"
}
}

View File

@ -17,7 +17,6 @@
"updateFailed": "更新內容失敗,請手動刷新頁面以查看新的 AI 工作摘要"
},
"tabs": {
"feedback": "💬 回饋",
"summary": "📋 AI 摘要",
"commands": "⚡ 命令",
"command": "⚡ 命令",
@ -53,7 +52,11 @@
"processingFeedback": "正在處理中,請稍候",
"connectingMessage": "WebSocket 連接中,回饋將在連接就緒後自動提交...",
"invalidState": "當前狀態不允許提交",
"sendFailed": "發送失敗,請重試"
"sendFailed": "發送失敗,請重試",
"noContent": "沒有可複製的內容",
"copySuccess": "內容已複製到剪貼板",
"copyFailed": "複製失敗",
"provideTextOrImage": "請提供回饋文字或上傳圖片"
},
"summary": {
"title": "📋 AI 工作摘要",
@ -93,18 +96,34 @@
"running": "執行中...",
"completed": "執行完成",
"error": "執行錯誤",
"history": "命令歷史"
"history": "命令歷史",
"autoCommand": {
"title": "🤖 自動執行命令設定",
"description": "設定在特定時機自動執行的命令",
"enabled": "啟用自動執行命令",
"onNewSession": "新會話建立時執行",
"onNewSessionPlaceholder": "例如pwd 或 git status",
"onFeedbackSubmit": "提交回饋後執行",
"onFeedbackSubmitPlaceholder": "例如echo '回饋已提交'",
"testOnNewSession": "測試新會話命令",
"testOnFeedbackSubmit": "測試回饋提交命令",
"help": "這些命令會在對應的時機自動執行。留空表示不執行任何命令。"
},
"autoSettings": {
"exampleNewSession": "💡 範例pwd, git status, ls -la",
"exampleFeedback": "💡 範例date, echo \"Done\", git log -1"
}
},
"combined": {
"summaryTitle": "📋 AI 工作摘要",
"feedbackTitle": "💬 提供回饋"
},
"settings": {
"settingsUI": {
"title": "⚙️ 設定",
"language": "語言",
"language": "🌍 語言",
"currentLanguage": "當前語言",
"languageDesc": "選擇界面顯示語言",
"interface": "介面設定",
"interface": "🎨 介面設定",
"layoutMode": "界面佈局模式",
"layoutModeDesc": "選擇 AI 摘要和回饋輸入的顯示方式",
"combinedVertical": "垂直佈局",
@ -115,7 +134,7 @@
"autoCloseDesc": "提交回饋後自動關閉頁面",
"theme": "主題",
"notifications": "通知",
"advanced": "進階設定",
"advanced": "🔧 進階設定",
"save": "儲存設定",
"reset": "重置設定",
"resetDesc": "清除所有已保存的設定,恢復到預設狀態",
@ -124,7 +143,14 @@
"resetError": "重置設定時發生錯誤",
"timeout": "連線逾時 (秒)",
"autorefresh": "自動重新整理",
"debug": "除錯模式"
"debug": "除錯模式",
"autoCommitNoPrompt": "請先選擇一個提示詞作為自動提交內容",
"sessionTimeoutTitle": "⏱️ 會話超時設定",
"sessionTimeoutEnable": "啟用會話超時",
"sessionTimeoutEnableDesc": "啟用後,會話將在指定時間後自動關閉",
"sessionTimeoutDuration": "超時時間(秒)",
"sessionTimeoutDurationDesc": "設定會話超時時間範圍300-86400 秒5分鐘-24小時",
"sessionTimeoutSeconds": "秒"
},
"languages": {
"zh-TW": "繁體中文",
@ -162,6 +188,10 @@
"submitted": {
"title": "回饋已提交",
"message": "等待下次 MCP 調用"
},
"completed": {
"title": "已完成",
"message": "會話已完成"
}
},
"notifications": {
@ -194,11 +224,13 @@
"upload": "上傳",
"download": "下載"
},
"session": {
"sessionTimeout": {
"timeout": "⏰ 會話已超時,介面將自動關閉",
"timeoutWarning": "會話即將超時",
"timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。",
"closing": "正在關閉..."
"closing": "正在關閉...",
"triggered": "會話已超時,程序即將關閉",
"label": "會話超時:"
},
"autoRefresh": {
"enable": "自動檢測",
@ -240,22 +272,30 @@
"noSummary": "暫無摘要",
"unknown": "未知"
},
"noSummary": "無摘要"
"noSummary": "無摘要",
"copySessionContent": "複製會話內容",
"copyUserContent": "複製用戶內容",
"exportSession": "匯出此會話"
},
"sessionHistory": {
"management": {
"title": "會話歷史管理",
"title": "📚 會話歷史管理",
"retentionPeriod": "保存期限",
"retentionHours": "小時",
"export": "匯出",
"clear": "清空",
"exportAll": "匯出全部",
"exportAllTitle": "匯出所有會話歷史到檔案",
"exportSingle": "匯出此會話",
"confirmClear": "確定要清空所有會話歷史嗎?",
"exportSuccess": "會話歷史已匯出",
"clearSuccess": "會話歷史已清空",
"clearTitle": "清空所有會話歷史記錄",
"description": "管理本地儲存的會話歷史記錄,包括保存期限設定和資料匯出功能",
"exportDescription": "匯出或清空本地儲存的會話歷史記錄"
"exportDescription": "匯出或清空本地儲存的會話歷史記錄",
"exportFailed": "匯出失敗: {error}",
"clearFailed": "清空失敗: {error}",
"clearFailedGeneric": "清空失敗"
},
"retention": {
"24hours": "24 小時",
@ -287,8 +327,17 @@
"imageCount": "圖片數量",
"timestamp": "時間戳記",
"clearAll": "清空訊息記錄",
"clearAllTitle": "清空所有會話的用戶訊息記錄",
"confirmClearAll": "確定要清空所有會話的用戶訊息記錄嗎?此操作無法復原。",
"clearSuccess": "用戶訊息記錄已清空"
},
"noActiveSession": "目前沒有活躍的會話數據",
"sessionNotFound": "找不到會話資料",
"currentSession": {
"noData": "沒有當前會話數據",
"noUserMessages": "當前會話沒有用戶消息記錄",
"copyFailed": "複製失敗,請重試",
"dataManagerNotInit": "數據管理器未初始化"
}
},
"connectionMonitor": {
@ -329,7 +378,7 @@
},
"prompts": {
"management": {
"title": "常用提示詞管理",
"title": "📝 常用提示詞管理",
"description": "管理您的常用提示詞模板,可在回饋輸入時快速選用",
"addNew": "新增提示詞",
"edit": "編輯",
@ -379,6 +428,7 @@
},
"about": {
"title": " 關於",
"description": "一個強大的 MCP 伺服器,為 AI 輔助開發工具提供人在回路的互動回饋功能。支援 Web UI 介面,並具備圖片上傳、命令執行、多語言等豐富功能。",
"appInfo": "應用程式資訊",
"version": "版本",
"projectLinks": "專案連結",
@ -393,7 +443,7 @@
},
"images": {
"settings": {
"title": "圖片設定",
"title": "🖼️ 圖片設定",
"sizeLimit": "圖片大小限制",
"sizeLimitDesc": "設定上傳圖片的最大檔案大小限制",
"sizeLimitOptions": {
@ -412,7 +462,7 @@
"sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。"
},
"autoSubmit": {
"title": "自動定時提交",
"title": "自動定時提交",
"enable": "啟用自動提交",
"enableDesc": "啟用後將在指定時間自動提交選定的提示詞內容",
"timeout": "倒數時間(秒)",
@ -425,11 +475,14 @@
"enabled": "已啟用",
"disabled": "已停用",
"executing": "正在執行自動提交...",
"countdownLabel": "提交倒數"
"countdownLabel": "提交倒數",
"pauseCountdown": "暫停倒數",
"resumeCountdown": "恢復倒數",
"paused": "自動提交已暫停"
},
"audio": {
"notification": {
"title": "音效通知設定",
"title": "🔊 音效通知設定",
"description": "設定會話更新時的音效通知",
"enabled": "啟用音效通知",
"enabledDesc": "啟用後將在有新會話更新時播放音效通知",
@ -462,5 +515,135 @@
"testPlaying": "正在播放測試音效",
"audioNotFound": "找不到選擇的音效"
}
},
"stats": {
"detailedStats": "詳細統計資訊"
},
"notification": {
"title": "瀏覽器通知",
"settingLabel": "瀏覽器通知",
"description": "新會話建立時通知(僅在背景執行時)",
"enabled": "通知已啟用 ✅",
"disabled": "通知已關閉",
"permissionRequired": "需要通知權限才能啟用此功能",
"permissionDenied": "瀏覽器已封鎖通知,請在瀏覽器設定中允許",
"permissionGranted": "已授權",
"permissionDeniedStatus": "已拒絕(請在瀏覽器設定中修改)",
"permissionDefault": "尚未設定",
"notSupported": "您的瀏覽器不支援通知功能",
"enableFailed": "啟用通知失敗",
"test": "發送測試通知",
"testTitle": "測試通知",
"testDescription": "發送測試通知以確認功能正常",
"autoplayBlocked": "瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知",
"triggerTitle": "通知觸發情境",
"triggerDescription": "選擇何時接收通知",
"triggerModeUpdated": "通知觸發模式已更新",
"trigger": {
"focusLost": "視窗失去焦點時(切換到其他應用程式)",
"tabSwitch": "切換到其他標籤頁時",
"background": "視窗最小化或隱藏時",
"always": "總是通知(包括前景)"
},
"browser": {
"title": "MCP Feedback - 新會話",
"ready": "準備就緒",
"unknownProject": "未知專案",
"testTitle": "測試通知",
"testBody": "這是一個測試通知5秒後將自動關閉",
"notSupported": "您的瀏覽器不支援通知功能",
"permissionRequired": "請先授權通知權限",
"criticalTitle": "MCP Feedback - 警告"
}
},
"system": {
"connectionEstablished": "WebSocket 連接已建立",
"connectionLost": "WebSocket 連接已斷開",
"connectionReconnecting": "正在重新連接...",
"connectionReconnected": "已重新連接",
"connectionFailed": "連接失敗",
"websocketError": "WebSocket 錯誤",
"memoryPressure": "記憶體壓力清理",
"shutdown": "系統關閉",
"processKilled": "進程已終止",
"heartbeatStopped": "心跳已停止",
"websocketReady": "WebSocket 已就緒"
},
"session": {
"noActiveSession": "沒有活躍會話",
"created": "新的 MCP 會話已創建,頁面將自動刷新",
"updated": "會話已更新",
"expired": "會話已過期",
"timeout": "會話已超時",
"cleaned": "會話已清理",
"feedbackSubmitted": "反饋已成功提交",
"userMessageRecorded": "用戶消息已記錄",
"historySaved": "會話歷史已保存({{count}} 個會話)",
"historyLoaded": "會話歷史已載入"
},
"settingsAPI": {
"saved": "設定已保存",
"loaded": "設定已載入",
"cleared": "設定已清除",
"saveFailed": "保存失敗",
"loadFailed": "載入失敗",
"clearFailed": "清除失敗",
"setFailed": "設定失敗",
"invalidValue": "無效的設定值",
"logLevelUpdated": "日誌等級已更新",
"invalidLogLevel": "無效的日誌等級,必須是 DEBUG, INFO, WARN, ERROR 之一"
},
"file": {
"uploadSuccess": "檔案上傳成功",
"uploadFailed": "檔案上傳失敗",
"sizeTooLarge": "檔案大小超過限制",
"typeNotSupported": "不支援的檔案類型",
"processing": "正在處理檔案...",
"removed": "檔案已移除"
},
"prompt": {
"saved": "提示詞已保存",
"deleted": "提示詞已刪除",
"applied": "已套用提示詞:{{name}}",
"importSuccess": "提示詞匯入成功",
"importFailed": "提示詞匯入失敗",
"exportSuccess": "提示詞匯出成功",
"validationFailed": "提示詞驗證失敗"
},
"error": {
"generic": "發生錯誤:{{error}}",
"network": "網絡錯誤",
"server": "伺服器錯誤",
"timeout": "操作超時",
"invalidInput": "輸入無效",
"operationFailed": "操作失敗",
"userMessageFailed": "添加用戶消息失敗",
"getSessionsFailed": "獲取會話列表失敗",
"getLogLevelFailed": "獲取日誌等級失敗",
"command": "命令執行錯誤",
"resourceCleanup": "資源清理錯誤",
"processing": "處理過程錯誤"
},
"commandStatus": {
"executing": "正在執行命令...",
"completed": "命令執行完成",
"failed": "命令執行失敗",
"outputReceived": "已接收輸出",
"invalid": "無效的命令",
"error": "命令執行錯誤"
},
"utils": {
"copySuccess": "已複製到剪貼板",
"copyError": "複製失敗"
},
"fileUpload": {
"fileSizeExceeded": "圖片大小超過限制 ({limit}): {filename}",
"maxFilesExceeded": "最多只能上傳 {maxFiles} 個檔案",
"processingFailed": "檔案處理失敗,請重試"
},
"aria": {
"toggleAutoSubmit": "切換自動提交",
"toggleNotification": "切換通知",
"toggleAudioNotification": "切換音效通知"
}
}

View File

@ -23,7 +23,6 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from ..debug import web_debug_log as debug_log
from ..i18n import get_i18n_manager
from ..utils.error_handler import ErrorHandler, ErrorType
from ..utils.memory_monitor import get_memory_monitor
from .models import CleanupReason, SessionStatus, WebFeedbackSession
@ -37,7 +36,14 @@ class WebUIManager:
"""Web UI 管理器 - 重構為單一活躍會話模式"""
def __init__(self, host: str = "127.0.0.1", port: int | None = None):
self.host = host
# 確定偏好主機:環境變數 > 參數 > 預設值 127.0.0.1
env_host = os.getenv("MCP_WEB_HOST")
if env_host:
self.host = env_host
debug_log(f"使用環境變數指定的主機: {self.host}")
else:
self.host = host
debug_log(f"未設定 MCP_WEB_HOST 環境變數,使用預設主機 {self.host}")
# 確定偏好端口:環境變數 > 參數 > 預設值 8765
preferred_port = 8765
@ -71,6 +77,18 @@ class WebUIManager:
if port is not None:
# 如果明確指定了端口,使用指定的端口
self.port = port
# 檢查指定端口是否可用
if not PortManager.is_port_available(self.host, self.port):
debug_log(f"警告:指定的端口 {self.port} 可能已被佔用")
# 在測試模式下,嘗試尋找替代端口
if os.environ.get("MCP_TEST_MODE", "").lower() == "true":
debug_log("測試模式:自動尋找替代端口")
original_port = self.port
self.port = PortManager.find_free_port_enhanced(
preferred_port=self.port, auto_cleanup=False, host=self.host
)
if self.port != original_port:
debug_log(f"自動切換到可用端口: {original_port}{self.port}")
elif preferred_port == 0:
# 如果偏好端口為 0使用系統自動分配
import socket
@ -130,7 +148,7 @@ class WebUIManager:
def _init_basic_components(self):
"""同步初始化基本組件"""
# 基本組件初始化(必須同步)
self.i18n = get_i18n_manager()
# 移除 i18n 管理器,因為翻譯已移至前端
# 設置靜態文件和模板(必須同步)
self._setup_static_files()
@ -174,9 +192,8 @@ class WebUIManager:
def preload_i18n():
try:
# 觸發翻譯載入(如果尚未載入)
self.i18n.get_supported_languages()
debug_log("I18N 資源預載入完成")
# I18N 在前端處理,這裡只記錄預載入完成
debug_log("I18N 資源預載入完成(前端處理)")
return True
except Exception as e:
debug_log(f"I18N 資源預載入失敗: {e}")
@ -311,25 +328,52 @@ class WebUIManager:
def create_session(self, project_directory: str, summary: str) -> str:
"""創建新的回饋會話 - 重構為單一活躍會話模式,保留標籤頁狀態"""
# 保存舊會話的 WebSocket 連接以便發送更新通知
# 保存舊會話的引用和 WebSocket 連接
old_session = self.current_session
old_websocket = None
if self.current_session and self.current_session.websocket:
old_websocket = self.current_session.websocket
if old_session and old_session.websocket:
old_websocket = old_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)
# 如果有舊會話,處理狀態轉換和清理
if old_session:
debug_log(
f"處理舊會話 {old_session.session_id} 的狀態轉換,當前狀態: {old_session.status.value}"
)
# 保存標籤頁狀態到全局
if hasattr(old_session, "active_tabs"):
self._merge_tabs_to_global(old_session.active_tabs)
# 如果舊會話是已提交狀態,進入下一步(已完成)
if old_session.status == SessionStatus.FEEDBACK_SUBMITTED:
debug_log(
f"舊會話 {old_session.session_id} 進入下一步:已提交 → 已完成"
)
success = old_session.next_step("反饋已處理,會話完成")
if success:
debug_log(f"✅ 舊會話 {old_session.session_id} 成功進入已完成狀態")
else:
debug_log(f"❌ 舊會話 {old_session.session_id} 無法進入下一步")
else:
debug_log(
f"舊會話 {old_session.session_id} 狀態為 {old_session.status.value},無需轉換"
)
# 確保舊會話仍在字典中用於API獲取
if old_session.session_id in self.sessions:
debug_log(f"舊會話 {old_session.session_id} 仍在會話字典中")
else:
debug_log(f"⚠️ 舊會話 {old_session.session_id} 不在會話字典中,重新添加")
self.sessions[old_session.session_id] = old_session
# 同步清理會話資源(但保留 WebSocket 連接)
old_session._cleanup_sync()
# 將全局標籤頁狀態繼承到新會話
session.active_tabs = self.global_active_tabs.copy()
@ -341,25 +385,11 @@ class WebUIManager:
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 連接,準備發送會話更新通知")
# 立即發送會話更新通知
import asyncio
try:
# 在後台任務中發送通知並轉移連接
asyncio.create_task(self._send_immediate_session_update())
except Exception as e:
debug_log(f"創建會話更新任務失敗: {e}")
# 即使任務創建失敗,也要嘗試直接轉移連接
session.websocket = old_websocket
debug_log("任務創建失敗,直接轉移 WebSocket 連接到新會話")
self._pending_session_update = True
# 直接轉移連接到新會話,消息發送由 smart_open_browser 統一處理
session.websocket = old_websocket
debug_log("已將舊 WebSocket 連接轉移到新會話")
else:
# 沒有舊連接,標記需要發送會話更新通知(當新 WebSocket 連接建立時)
self._pending_session_update = True
@ -454,9 +484,48 @@ class WebUIManager:
def run_server_with_retry():
max_retries = 5
retry_count = 0
original_port = self.port
while retry_count < max_retries:
try:
# 在嘗試啟動前先檢查端口是否可用
if not PortManager.is_port_available(self.host, self.port):
debug_log(f"端口 {self.port} 已被佔用,自動尋找替代端口")
# 查找占用端口的進程信息
process_info = PortManager.find_process_using_port(self.port)
if process_info:
debug_log(
f"端口 {self.port} 被進程 {process_info['name']} "
f"(PID: {process_info['pid']}) 佔用"
)
# 自動尋找新端口
try:
new_port = PortManager.find_free_port_enhanced(
preferred_port=self.port,
auto_cleanup=False, # 不自動清理其他進程
host=self.host,
)
debug_log(f"自動切換端口: {self.port}{new_port}")
self.port = new_port
except RuntimeError as port_error:
error_id = ErrorHandler.log_error_with_context(
port_error,
context={
"operation": "端口查找",
"original_port": original_port,
"current_port": self.port,
},
error_type=ErrorType.NETWORK,
)
debug_log(
f"無法找到可用端口 [錯誤ID: {error_id}]: {port_error}"
)
raise RuntimeError(
f"無法找到可用端口,原始端口 {original_port} 被佔用"
) from port_error
debug_log(
f"嘗試啟動伺服器在 {self.host}:{self.port} (嘗試 {retry_count + 1}/{max_retries})"
)
@ -483,37 +552,27 @@ class WebUIManager:
)
asyncio.run(serve_with_async_init())
# 成功啟動,顯示最終使用的端口
if self.port != original_port:
debug_log(
f"✅ 服務器成功啟動在替代端口 {self.port} (原端口 {original_port} 被佔用)"
)
break
except OSError as e:
if e.errno == 10048: # Windows: 位址已在使用中
if e.errno in {
10048,
98,
}: # Windows: 10048, Linux: 98 (位址已在使用中)
retry_count += 1
if retry_count < max_retries:
debug_log(
f"端口 {self.port} 被占用,使用增強端口管理查找新端口"
f"端口 {self.port} 啟動失敗 (OSError),嘗試下一個端口"
)
# 使用增強的端口管理查找新端口
try:
self.port = PortManager.find_free_port_enhanced(
preferred_port=self.port + 1,
auto_cleanup=False, # 啟動時不自動清理,避免誤殺其他服務
host=self.host,
)
debug_log(f"找到新的可用端口: {self.port}")
except RuntimeError as port_error:
# 使用統一錯誤處理
error_id = ErrorHandler.log_error_with_context(
port_error,
context={
"operation": "端口查找",
"current_port": self.port,
},
error_type=ErrorType.NETWORK,
)
debug_log(
f"無法找到可用端口 [錯誤ID: {error_id}]: {port_error}"
)
break
# 嘗試下一個端口
self.port = self.port + 1
else:
debug_log("已達到最大重試次數,無法啟動伺服器")
break
@ -577,8 +636,14 @@ class WebUIManager:
has_active_tabs = await self._check_active_tabs()
if has_active_tabs:
debug_log("檢測到活躍標籤頁,發送刷新通知")
debug_log(f"向現有標籤頁發送刷新通知:{url}")
# 向現有標籤頁發送刷新通知
refresh_success = await self.notify_existing_tab_to_refresh()
debug_log(f"刷新通知發送結果: {refresh_success}")
debug_log("檢測到活躍標籤頁,不開啟新瀏覽器視窗")
debug_log(f"用戶可以在現有標籤頁中查看更新:{url}")
return True
# 沒有活躍標籤頁,開啟新瀏覽器視窗
@ -669,106 +734,6 @@ class WebUIManager:
else:
debug_log("沒有活躍的桌面應用程式實例")
async def notify_session_update(self, session):
"""向活躍標籤頁發送會話更新通知"""
try:
# 檢查是否有活躍的 WebSocket 連接
if session.websocket:
# 直接通過當前會話的 WebSocket 發送
await session.websocket.send_json(
{
"type": "session_updated",
"message": "新會話已創建,正在更新頁面內容",
"session_info": {
"project_directory": session.project_directory,
"summary": session.summary,
"session_id": session.session_id,
},
}
)
debug_log("會話更新通知已通過 WebSocket 發送")
else:
# 沒有活躍連接,設置待更新標記
self._pending_session_update = True
debug_log("沒有活躍 WebSocket 連接,設置待更新標記")
except Exception as e:
debug_log(f"發送會話更新通知失敗: {e}")
# 設置待更新標記作為備用方案
self._pending_session_update = True
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
# 改進的連接有效性檢查
websocket_valid = False
if old_websocket:
try:
# 檢查 WebSocket 連接狀態
if hasattr(old_websocket, "client_state"):
websocket_valid = (
old_websocket.client_state
!= old_websocket.client_state.DISCONNECTED
)
else:
# 如果沒有 client_state 屬性,嘗試發送測試消息來檢查連接
websocket_valid = True
except Exception as check_error:
debug_log(f"檢查 WebSocket 連接狀態失敗: {check_error}")
websocket_valid = False
if websocket_valid:
try:
# 發送會話更新通知
await old_websocket.send_json(
{
"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 連接發送會話更新通知")
# 延遲一小段時間讓前端處理消息
await asyncio.sleep(0.2)
# 將 WebSocket 連接轉移到新會話
new_session.websocket = old_websocket
debug_log("已將 WebSocket 連接轉移到新會話")
except Exception as send_error:
debug_log(f"發送會話更新通知失敗: {send_error}")
# 如果發送失敗,仍然嘗試轉移連接
new_session.websocket = old_websocket
debug_log("發送失敗但仍轉移 WebSocket 連接到新會話")
else:
debug_log("舊 WebSocket 連接無效,設置待更新標記")
self._pending_session_update = True
# 清理臨時變數
delattr(self, "_old_websocket_for_update")
delattr(self, "_new_session_for_update")
else:
# 沒有舊連接,設置待更新標記
self._pending_session_update = True
debug_log("沒有舊 WebSocket 連接,設置待更新標記")
except Exception as e:
debug_log(f"立即發送會話更新通知失敗: {e}")
# 回退到待更新標記
self._pending_session_update = True
async def _safe_close_websocket(self, websocket):
"""安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用"""
if not websocket:
@ -792,40 +757,102 @@ class WebUIManager:
except Exception as e:
debug_log(f"檢查 WebSocket 連接狀態時發生錯誤: {e}")
async def _check_active_tabs(self) -> bool:
"""檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API"""
async def notify_existing_tab_to_refresh(self) -> bool:
"""通知現有標籤頁刷新顯示新會話內容
Returns:
bool: True 表示成功發送False 表示失敗
"""
try:
# 首先檢查全局標籤頁狀態
global_count = self.get_global_active_tabs_count()
if global_count > 0:
debug_log(f"檢測到 {global_count} 個全局活躍標籤頁")
if not self.current_session or not self.current_session.websocket:
debug_log("沒有活躍的WebSocket連接無法發送刷新通知")
return False
# 構建刷新通知消息
refresh_message = {
"type": "session_updated",
"action": "new_session_created",
"messageCode": "session.created",
"session_info": {
"session_id": self.current_session.session_id,
"project_directory": self.current_session.project_directory,
"summary": self.current_session.summary,
"status": self.current_session.status.value,
},
}
# 發送刷新通知
await self.current_session.websocket.send_json(refresh_message)
debug_log(f"已向現有標籤頁發送刷新通知: {self.current_session.session_id}")
# 簡單等待一下讓消息發送完成
await asyncio.sleep(0.2)
debug_log("刷新通知發送完成")
return True
except Exception as e:
debug_log(f"發送刷新通知失敗: {e}")
return False
async def _check_active_tabs(self) -> bool:
"""檢查是否有活躍標籤頁 - 使用分層檢測機制"""
try:
# 快速檢測層:檢查 WebSocket 物件是否存在
if not self.current_session or not self.current_session.websocket:
debug_log("快速檢測:沒有當前會話或 WebSocket 連接")
return False
# 檢查心跳(如果有心跳記錄)
last_heartbeat = getattr(self.current_session, "last_heartbeat", None)
if last_heartbeat:
heartbeat_age = time.time() - last_heartbeat
if heartbeat_age > 10: # 超過 10 秒沒有心跳
debug_log(f"快速檢測:心跳超時 ({heartbeat_age:.1f}秒)")
# 可能連接已死,需要進一步檢測
else:
debug_log(f"快速檢測:心跳正常 ({heartbeat_age:.1f}秒前)")
return True # 心跳正常,認為連接活躍
# 準確檢測層:實際測試連接是否活著
try:
# 檢查 WebSocket 連接狀態
websocket = self.current_session.websocket
# 檢查連接是否已關閉
if hasattr(websocket, "client_state"):
try:
# 嘗試從 starlette 導入FastAPI 基於 Starlette
import starlette.websockets # type: ignore[import-not-found]
if hasattr(starlette.websockets, "WebSocketState"):
WebSocketState = starlette.websockets.WebSocketState
if websocket.client_state != WebSocketState.CONNECTED:
debug_log(
f"準確檢測WebSocket 狀態不是 CONNECTED而是 {websocket.client_state}"
)
# 清理死連接
self.current_session.websocket = None
return False
except ImportError:
# 如果導入失敗,使用替代方法
debug_log("無法導入 WebSocketState使用替代方法檢測連接")
# 跳過狀態檢查,直接測試連接
# 如果連接看起來是活的,嘗試發送 ping非阻塞
# 注意FastAPI WebSocket 沒有內建的 ping 方法,這裡使用自定義消息
await websocket.send_json({"type": "ping", "timestamp": time.time()})
debug_log("準確檢測:成功發送 ping 消息,連接是活躍的")
return True
# 如果全局狀態沒有活躍標籤頁,嘗試通過 API 檢查
# 等待一小段時間讓服務器完全啟動
await asyncio.sleep(0.5)
except Exception as e:
debug_log(f"準確檢測:連接測試失敗 - {e}")
# 連接已死,清理它
if self.current_session:
self.current_session.websocket = None
return False
# 調用活躍標籤頁 API
import aiohttp
timeout = aiohttp.ClientTimeout(total=2)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(
f"{self.get_server_url()}/api/active-tabs"
) as response:
if response.status == 200:
data = await response.json()
tab_count = data.get("count", 0)
debug_log(f"API 檢測到 {tab_count} 個活躍標籤頁")
return bool(tab_count > 0)
debug_log(f"檢查活躍標籤頁失敗,狀態碼:{response.status}")
return False
except TimeoutError:
debug_log("檢查活躍標籤頁超時")
return False
except Exception as e:
debug_log(f"檢查活躍標籤頁時發生錯誤:{e}")
debug_log(f"檢查活躍連接時發生錯誤:{e}")
return False
def get_server_url(self) -> str:
@ -1085,7 +1112,7 @@ async def launch_web_feedback_ui(
"""
manager = get_web_ui_manager()
# 創建或更當前活躍會話
# 創建新會話每次AI調用都應該創建新會話
manager.create_session(project_directory, summary)
session = manager.get_current_session()
@ -1112,10 +1139,9 @@ async def launch_web_feedback_ui(
debug_log(f"[DEBUG] 服務器地址: {feedback_url}")
# 如果檢測到活躍標籤頁但沒有開啟新視窗,立即發送會話更新通知
# 如果檢測到活躍標籤頁,消息已在 smart_open_browser 中發送,無需額外處理
if has_active_tabs:
await manager._send_immediate_session_update()
debug_log("已向活躍標籤頁發送會話更新通知")
debug_log("檢測到活躍標籤頁,會話更新通知已發送")
try:
# 等待用戶回饋,傳遞 timeout 參數

View File

@ -26,18 +26,19 @@ from fastapi import WebSocket
from ...debug import web_debug_log as debug_log
from ...utils.error_handler import ErrorHandler, ErrorType
from ...utils.resource_manager import get_resource_manager, register_process
from ..constants import get_message_code
class SessionStatus(Enum):
"""會話狀態枚舉"""
"""會話狀態枚舉 - 單向流轉設計"""
WAITING = "waiting" # 等待中
ACTIVE = "active" # 活躍
ACTIVE = "active" # 活躍狀態
FEEDBACK_SUBMITTED = "feedback_submitted" # 已提交反饋
COMPLETED = "completed" # 已完成
TIMEOUT = "timeout" # 超時
ERROR = "error" # 錯誤
EXPIRED = "expired" # 已過期
ERROR = "error" # 錯誤(終態)
TIMEOUT = "timeout" # 超時(終態)
EXPIRED = "expired" # 已過期(終態)
class CleanupReason(Enum):
@ -63,6 +64,9 @@ SUPPORTED_IMAGE_TYPES = {
}
TEMP_DIR = Path.home() / ".cache" / "interactive-feedback-mcp-web"
# 訊息代碼現在從統一的常量文件導入
# 使用 get_message_code 函數來獲取訊息代碼
def _safe_parse_command(command: str) -> list[str]:
"""
@ -133,7 +137,9 @@ class WebFeedbackSession:
self.feedback_completed = threading.Event()
self.process: subprocess.Popen | None = None
self.command_logs: list[str] = []
self.user_messages: list[dict] = [] # 用戶消息記錄
self._cleanup_done = False # 防止重複清理
# 移除語言設定,改由前端處理
# 新增:會話狀態管理
self.status = SessionStatus.WAITING
@ -141,6 +147,7 @@ class WebFeedbackSession:
# 統一使用 time.time() 以避免時間基準不一致
self.created_at = time.time()
self.last_activity = self.created_at
self.last_heartbeat = None # 記錄最後一次心跳時間
# 新增:自動清理配置
self.auto_cleanup_delay = auto_cleanup_delay # 自動清理延遲時間(秒)
@ -161,6 +168,11 @@ class WebFeedbackSession:
# 新增:活躍標籤頁管理
self.active_tabs: dict[str, Any] = {}
# 新增:用戶設定的會話超時
self.user_timeout_enabled = False
self.user_timeout_seconds = 3600 # 預設 1 小時
self.user_timeout_timer: threading.Timer | None = None
# 確保臨時目錄存在
TEMP_DIR.mkdir(parents=True, exist_ok=True)
@ -174,21 +186,101 @@ class WebFeedbackSession:
f"會話 {self.session_id} 初始化完成,自動清理延遲: {auto_cleanup_delay}秒,最大空閒: {max_idle_time}"
)
def update_status(self, status: SessionStatus, message: str | None = None):
"""更新會話狀態"""
self.status = status
def get_message_code(self, key: str) -> str:
"""
獲取訊息代碼
Args:
key: 訊息 key
Returns:
訊息代碼用於前端 i18n
"""
return get_message_code(key)
def next_step(self, message: str | None = None) -> bool:
"""進入下一個狀態 - 單向流轉,不可倒退"""
old_status = self.status
# 定義狀態流轉路徑
next_status_map = {
SessionStatus.WAITING: SessionStatus.ACTIVE,
SessionStatus.ACTIVE: SessionStatus.FEEDBACK_SUBMITTED,
SessionStatus.FEEDBACK_SUBMITTED: SessionStatus.COMPLETED,
SessionStatus.COMPLETED: None, # 終態
SessionStatus.ERROR: None, # 終態
SessionStatus.TIMEOUT: None, # 終態
SessionStatus.EXPIRED: None, # 終態
}
next_status = next_status_map.get(self.status)
if next_status is None:
debug_log(
f"⚠️ 會話 {self.session_id} 已處於終態 {self.status.value},無法進入下一步"
)
return False
# 執行狀態轉換
self.status = next_status
if message:
self.status_message = message
# 統一使用 time.time()
else:
# 默認消息
default_messages = {
SessionStatus.ACTIVE: "會話已啟動",
SessionStatus.FEEDBACK_SUBMITTED: "用戶已提交反饋",
SessionStatus.COMPLETED: "會話已完成",
}
self.status_message = default_messages.get(next_status, "狀態已更新")
self.last_activity = time.time()
# 如果會話變為活躍狀態,重置清理定時器
if status in [SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED]:
# 如果會話變為已提交狀態,重置清理定時器
if next_status == SessionStatus.FEEDBACK_SUBMITTED:
self._schedule_auto_cleanup()
debug_log(
f"會話 {self.session_id} 狀態更新: {status.value} - {self.status_message}"
f"會話 {self.session_id} 狀態流轉: {old_status.value}{next_status.value} - {self.status_message}"
)
return True
def set_error(self, message: str = "會話發生錯誤") -> bool:
"""設置錯誤狀態(特殊方法,可從任何狀態進入)"""
old_status = self.status
self.status = SessionStatus.ERROR
self.status_message = message
self.last_activity = time.time()
debug_log(
f"❌ 會話 {self.session_id} 設置為錯誤狀態: {old_status.value}{self.status.value} - {message}"
)
return True
def set_expired(self, message: str = "會話已過期") -> bool:
"""設置過期狀態(特殊方法,可從任何狀態進入)"""
old_status = self.status
self.status = SessionStatus.EXPIRED
self.status_message = message
self.last_activity = time.time()
debug_log(
f"⏰ 會話 {self.session_id} 設置為過期狀態: {old_status.value}{self.status.value} - {message}"
)
return True
def can_proceed(self) -> bool:
"""檢查是否可以進入下一步"""
return self.status in [SessionStatus.WAITING, SessionStatus.FEEDBACK_SUBMITTED]
def is_terminal(self) -> bool:
"""檢查是否處於終態"""
return self.status in [
SessionStatus.COMPLETED,
SessionStatus.ERROR,
SessionStatus.TIMEOUT,
SessionStatus.EXPIRED,
]
def get_status_info(self) -> dict[str, Any]:
"""獲取會話狀態信息"""
@ -334,6 +426,39 @@ class WebFeedbackSession:
)
return stats
def update_timeout_settings(self, enabled: bool, timeout_seconds: int = 3600):
"""
更新用戶設定的會話超時
Args:
enabled: 是否啟用超時
timeout_seconds: 超時秒數
"""
debug_log(f"更新會話超時設定: enabled={enabled}, seconds={timeout_seconds}")
# 先停止現有的計時器
if self.user_timeout_timer:
self.user_timeout_timer.cancel()
self.user_timeout_timer = None
self.user_timeout_enabled = enabled
self.user_timeout_seconds = timeout_seconds
# 如果啟用且會話還在等待中,啟動計時器
if enabled and self.status == SessionStatus.WAITING:
def timeout_handler():
debug_log(f"用戶設定的超時已到: {self.session_id}")
# 設置超時標誌
self.status = SessionStatus.TIMEOUT
self.status_message = "用戶設定的會話超時"
# 設置完成事件,讓 wait_for_feedback 結束等待
self.feedback_completed.set()
self.user_timeout_timer = threading.Timer(timeout_seconds, timeout_handler)
self.user_timeout_timer.start()
debug_log(f"已啟動用戶超時計時器: {timeout_seconds}")
async def wait_for_feedback(self, timeout: int = 600) -> dict[str, Any]:
"""
等待用戶回饋包含圖片支援超時自動清理
@ -363,6 +488,12 @@ class WebFeedbackSession:
completed = await loop.run_in_executor(None, wait_in_thread)
if completed:
# 檢查是否是用戶設定的超時
if self.status == SessionStatus.TIMEOUT and self.user_timeout_enabled:
debug_log(f"會話 {self.session_id} 因用戶設定超時而結束")
await self._cleanup_resources_on_timeout()
raise TimeoutError("會話已因用戶設定的超時而關閉")
debug_log(f"會話 {self.session_id} 收到用戶回饋")
return {
"logs": "\n".join(self.command_logs),
@ -404,10 +535,8 @@ class WebFeedbackSession:
self.settings = settings or {}
self.images = self._process_images(images)
# 更新狀態為已提交反饋
self.update_status(
SessionStatus.FEEDBACK_SUBMITTED, "已送出反饋,等待下次 MCP 調用"
)
# 進入下一步:等待中 → 已提交反饋
self.next_step("已送出反饋,等待下次 MCP 調用")
self.feedback_completed.set()
@ -416,8 +545,9 @@ class WebFeedbackSession:
try:
await self.websocket.send_json(
{
"type": "feedback_received",
"message": "反饋已成功提交",
"type": "notification",
"code": self.get_message_code("FEEDBACK_SUBMITTED"),
"severity": "success",
"status": self.status.value,
}
)
@ -443,6 +573,24 @@ class WebFeedbackSession:
# 重構:不再自動關閉 WebSocket保持連接以支援頁面持久性
def add_user_message(self, message_data: dict[str, Any]) -> None:
"""添加用戶消息記錄"""
import time
# 創建用戶消息記錄
user_message = {
"timestamp": int(time.time() * 1000), # 毫秒時間戳
"content": message_data.get("content", ""),
"images": message_data.get("images", []),
"submission_method": message_data.get("submission_method", "manual"),
"type": "feedback",
}
self.user_messages.append(user_message)
debug_log(
f"會話 {self.session_id} 添加用戶消息,總數: {len(self.user_messages)}"
)
def _process_images(self, images: list[dict]) -> list[dict]:
"""
處理圖片數據轉換為統一格式
@ -649,24 +797,33 @@ class WebFeedbackSession:
self.cleanup_timer = None
resources_cleaned += 1
# 1.5. 取消用戶超時計時器
if self.user_timeout_timer:
self.user_timeout_timer.cancel()
self.user_timeout_timer = None
resources_cleaned += 1
# 2. 關閉 WebSocket 連接
if self.websocket:
try:
# 根據清理原因發送不同的通知消息
message_map = {
CleanupReason.TIMEOUT: "會話已超時,介面將自動關閉",
CleanupReason.EXPIRED: "會話已過期,介面將自動關閉",
CleanupReason.MEMORY_PRESSURE: "系統內存不足,會話將被清理",
CleanupReason.MANUAL: "會話已被手動清理",
CleanupReason.ERROR: "會話發生錯誤,將被清理",
CleanupReason.SHUTDOWN: "系統正在關閉,會話將被清理",
# 根據清理原因獲取訊息代碼
code_key_map = {
CleanupReason.TIMEOUT: "TIMEOUT_CLEANUP",
CleanupReason.EXPIRED: "EXPIRED_CLEANUP",
CleanupReason.MEMORY_PRESSURE: "MEMORY_PRESSURE_CLEANUP",
CleanupReason.MANUAL: "MANUAL_CLEANUP",
CleanupReason.ERROR: "ERROR_CLEANUP",
CleanupReason.SHUTDOWN: "SHUTDOWN_CLEANUP",
}
code_key = code_key_map.get(reason, "SESSION_CLEANUP")
await self.websocket.send_json(
{
"type": "session_cleanup",
"type": "notification",
"code": self.get_message_code(code_key),
"severity": "warning",
"reason": reason.value,
"message": message_map.get(reason, "會話將被清理"),
}
)
await asyncio.sleep(0.1) # 給前端一點時間處理消息

View File

@ -16,6 +16,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
from ... import __version__
from ...debug import web_debug_log as debug_log
from ..constants import get_message_code as get_msg_code
if TYPE_CHECKING:
@ -44,6 +45,11 @@ def load_user_layout_settings() -> str:
return "combined-vertical"
# 使用統一的訊息代碼系統
# 從 ..constants 導入的 get_msg_code 函數會處理所有訊息代碼
# 舊的 key 會自動映射到新的常量
def setup_routes(manager: "WebUIManager"):
"""設置路由"""
@ -79,7 +85,6 @@ def setup_routes(manager: "WebUIManager"):
"version": __version__,
"has_session": True,
"layout_mode": layout_mode,
"i18n": manager.i18n,
},
)
@ -113,16 +118,23 @@ def setup_routes(manager: "WebUIManager"):
return JSONResponse(content=translations)
@manager.app.get("/api/session-status")
async def get_session_status():
async def get_session_status(request: Request):
"""獲取當前會話狀態"""
current_session = manager.get_current_session()
# 從請求頭獲取客戶端語言
lang = (
request.headers.get("Accept-Language", "zh-TW").split(",")[0].split("-")[0]
)
if lang == "zh":
lang = "zh-TW"
if not current_session:
return JSONResponse(
content={
"has_session": False,
"status": "no_session",
"message": "沒有活躍會話",
"messageCode": get_msg_code("no_active_session"),
}
)
@ -139,12 +151,20 @@ def setup_routes(manager: "WebUIManager"):
)
@manager.app.get("/api/current-session")
async def get_current_session():
async def get_current_session(request: Request):
"""獲取當前會話詳細信息"""
current_session = manager.get_current_session()
# 從查詢參數獲取語言,如果沒有則從會話獲取,最後使用默認值
if not current_session:
return JSONResponse(status_code=404, content={"error": "沒有活躍會話"})
return JSONResponse(
status_code=404,
content={
"error": "No active session",
"messageCode": get_msg_code("no_active_session"),
},
)
return JSONResponse(
content={
@ -157,17 +177,98 @@ def setup_routes(manager: "WebUIManager"):
}
)
@manager.app.get("/api/all-sessions")
async def get_all_sessions(request: Request):
"""獲取所有會話的實時狀態"""
try:
sessions_data = []
# 獲取所有會話的實時狀態
for session_id, session in manager.sessions.items():
session_info = {
"session_id": session.session_id,
"project_directory": session.project_directory,
"summary": session.summary,
"status": session.status.value,
"status_message": session.status_message,
"created_at": int(session.created_at * 1000), # 轉換為毫秒
"last_activity": int(session.last_activity * 1000),
"feedback_completed": session.feedback_completed.is_set(),
"has_websocket": session.websocket is not None,
"is_current": session == manager.current_session,
"user_messages": session.user_messages, # 包含用戶消息記錄
}
sessions_data.append(session_info)
# 按創建時間排序(最新的在前)
sessions_data.sort(key=lambda x: x["created_at"], reverse=True)
debug_log(f"返回 {len(sessions_data)} 個會話的實時狀態")
return JSONResponse(content={"sessions": sessions_data})
except Exception as e:
debug_log(f"獲取所有會話狀態失敗: {e}")
return JSONResponse(
status_code=500,
content={
"error": f"Failed to get sessions: {e!s}",
"messageCode": get_msg_code("get_sessions_failed"),
},
)
@manager.app.post("/api/add-user-message")
async def add_user_message(request: Request):
"""添加用戶消息到當前會話"""
try:
data = await request.json()
current_session = manager.get_current_session()
if not current_session:
return JSONResponse(
status_code=404,
content={
"error": "No active session",
"messageCode": get_msg_code("no_active_session"),
},
)
# 添加用戶消息到會話
current_session.add_user_message(data)
debug_log(f"用戶消息已添加到會話 {current_session.session_id}")
return JSONResponse(
content={
"status": "success",
"messageCode": get_msg_code("user_message_recorded"),
}
)
except Exception as e:
debug_log(f"添加用戶消息失敗: {e}")
return JSONResponse(
status_code=500,
content={
"error": f"Failed to add user message: {e!s}",
"messageCode": get_msg_code("add_user_message_failed"),
},
)
@manager.app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
async def websocket_endpoint(websocket: WebSocket, lang: str = "zh-TW"):
"""WebSocket 端點 - 重構後移除 session_id 依賴"""
# 獲取當前活躍會話
session = manager.get_current_session()
if not session:
await websocket.close(code=4004, reason="沒有活躍會話")
await websocket.close(code=4004, reason="No active session")
return
await websocket.accept()
# 語言由前端處理,不需要在後端設置
debug_log(f"WebSocket 連接建立,語言由前端處理: {lang}")
# 檢查會話是否已有 WebSocket 連接
if session.websocket and session.websocket != websocket:
debug_log("會話已有 WebSocket 連接,替換為新連接")
@ -178,7 +279,10 @@ def setup_routes(manager: "WebUIManager"):
# 發送連接成功消息
try:
await websocket.send_json(
{"type": "connection_established", "message": "WebSocket 連接已建立"}
{
"type": "connection_established",
"messageCode": get_msg_code("websocket_connected"),
}
)
# 檢查是否有待發送的會話更新
@ -187,7 +291,8 @@ def setup_routes(manager: "WebUIManager"):
await websocket.send_json(
{
"type": "session_updated",
"message": "新會話已創建,正在更新頁面內容",
"action": "new_session_created",
"messageCode": get_msg_code("new_session_created"),
"session_info": {
"project_directory": session.project_directory,
"summary": session.summary,
@ -236,6 +341,7 @@ def setup_routes(manager: "WebUIManager"):
@manager.app.post("/api/save-settings")
async def save_settings(request: Request):
"""保存設定到檔案"""
try:
data = await request.json()
@ -250,18 +356,28 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"設定已保存到: {settings_file}")
return JSONResponse(content={"status": "success", "message": "設定已保存"})
return JSONResponse(
content={
"status": "success",
"messageCode": get_msg_code("settings_saved"),
}
)
except Exception as e:
debug_log(f"保存設定失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"保存失敗: {e!s}"},
content={
"status": "error",
"message": f"Save failed: {e!s}",
"messageCode": get_msg_code("save_failed"),
},
)
@manager.app.get("/api/load-settings")
async def load_settings():
async def load_settings(request: Request):
"""從檔案載入設定"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
@ -280,12 +396,17 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"載入設定失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"載入失敗: {e!s}"},
content={
"status": "error",
"message": f"Load failed: {e!s}",
"messageCode": get_msg_code("load_failed"),
},
)
@manager.app.post("/api/clear-settings")
async def clear_settings():
async def clear_settings(request: Request):
"""清除設定檔案"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
@ -297,18 +418,28 @@ def setup_routes(manager: "WebUIManager"):
else:
debug_log("設定檔案不存在,無需刪除")
return JSONResponse(content={"status": "success", "message": "設定已清除"})
return JSONResponse(
content={
"status": "success",
"messageCode": get_msg_code("settings_cleared"),
}
)
except Exception as e:
debug_log(f"清除設定失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"清除失敗: {e!s}"},
content={
"status": "error",
"message": f"Clear failed: {e!s}",
"messageCode": get_msg_code("clear_failed"),
},
)
@manager.app.get("/api/load-session-history")
async def load_session_history():
async def load_session_history(request: Request):
"""從檔案載入會話歷史"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
@ -330,7 +461,7 @@ def setup_routes(manager: "WebUIManager"):
sessions = history_data if isinstance(history_data, list) else []
last_cleanup = 0
# 回傳與 localStorage 格式相容的資料
# 回傳會話歷史資料
return JSONResponse(
content={"sessions": sessions, "lastCleanup": last_cleanup}
)
@ -342,12 +473,17 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"載入會話歷史失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"載入失敗: {e!s}"},
content={
"status": "error",
"message": f"Load failed: {e!s}",
"messageCode": get_msg_code("load_failed"),
},
)
@manager.app.post("/api/save-session-history")
async def save_session_history(request: Request):
"""保存會話歷史到檔案"""
try:
data = await request.json()
@ -364,11 +500,6 @@ def setup_routes(manager: "WebUIManager"):
"savedAt": int(time.time() * 1000), # 當前時間戳
}
# 如果是首次儲存且有 localStorage 遷移標記
if not history_file.exists() and data.get("migratedFrom") == "localStorage":
history_data["migratedFrom"] = "localStorage"
history_data["migratedAt"] = int(time.time() * 1000)
# 保存會話歷史到檔案
with open(history_file, "w", encoding="utf-8") as f:
json.dump(history_data, f, ensure_ascii=False, indent=2)
@ -380,7 +511,8 @@ def setup_routes(manager: "WebUIManager"):
return JSONResponse(
content={
"status": "success",
"message": f"會話歷史已保存({session_count} 個會話)",
"messageCode": get_msg_code("session_history_saved"),
"params": {"count": session_count},
}
)
@ -388,82 +520,99 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"保存會話歷史失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"保存失敗: {e!s}"},
content={
"status": "error",
"message": f"Save failed: {e!s}",
"messageCode": get_msg_code("save_failed"),
},
)
@manager.app.get("/api/active-tabs")
async def get_active_tabs():
"""獲取活躍標籤頁信息 - 優先使用全局狀態"""
current_time = time.time()
expired_threshold = 60
@manager.app.get("/api/log-level")
async def get_log_level(request: Request):
"""獲取日誌等級設定"""
# 清理過期的全局標籤頁
valid_global_tabs = {}
for tab_id, tab_info in manager.global_active_tabs.items():
if current_time - tab_info.get("last_seen", 0) <= expired_threshold:
valid_global_tabs[tab_id] = tab_info
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
settings_file = config_dir / "ui_settings.json"
manager.global_active_tabs = valid_global_tabs
if settings_file.exists():
with open(settings_file, encoding="utf-8") as f:
settings_data = json.load(f)
log_level = settings_data.get("logLevel", "INFO")
debug_log(f"從設定檔案載入日誌等級: {log_level}")
return JSONResponse(content={"logLevel": log_level})
else:
# 預設日誌等級
default_log_level = "INFO"
debug_log(f"使用預設日誌等級: {default_log_level}")
return JSONResponse(content={"logLevel": default_log_level})
# 如果有當前會話,也更新會話的標籤頁狀態
current_session = manager.get_current_session()
if current_session:
# 合併會話標籤頁到全局(如果有的話)
session_tabs = getattr(current_session, "active_tabs", {})
for tab_id, tab_info in session_tabs.items():
if current_time - tab_info.get("last_seen", 0) <= expired_threshold:
valid_global_tabs[tab_id] = tab_info
except Exception as e:
debug_log(f"獲取日誌等級失敗: {e}")
return JSONResponse(
status_code=500,
content={
"error": f"Failed to get log level: {e!s}",
"messageCode": get_msg_code("get_log_level_failed"),
},
)
# 更新會話的活躍標籤頁
current_session.active_tabs = valid_global_tabs.copy()
manager.global_active_tabs = valid_global_tabs
@manager.app.post("/api/log-level")
async def set_log_level(request: Request):
"""設定日誌等級"""
return JSONResponse(
content={
"has_session": current_session is not None,
"active_tabs": valid_global_tabs,
"count": len(valid_global_tabs),
}
)
@manager.app.post("/api/register-tab")
async def register_tab(request: Request):
"""註冊新標籤頁"""
try:
data = await request.json()
tab_id = data.get("tabId")
log_level = data.get("logLevel")
if not tab_id:
return JSONResponse(status_code=400, content={"error": "缺少 tabId"})
if not log_level or log_level not in ["DEBUG", "INFO", "WARN", "ERROR"]:
return JSONResponse(
status_code=400,
content={
"error": "Invalid log level",
"messageCode": get_msg_code("invalid_log_level"),
},
)
current_session = manager.get_current_session()
if not current_session:
return JSONResponse(status_code=404, content={"error": "沒有活躍會話"})
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
config_dir.mkdir(parents=True, exist_ok=True)
settings_file = config_dir / "ui_settings.json"
# 註冊標籤頁
tab_info = {
"timestamp": time.time() * 1000, # 毫秒時間戳
"last_seen": time.time(),
"registered_at": time.time(),
}
# 載入現有設定或創建新設定
settings_data = {}
if settings_file.exists():
with open(settings_file, encoding="utf-8") as f:
settings_data = json.load(f)
if not hasattr(current_session, "active_tabs"):
current_session.active_tabs = {}
# 更新日誌等級
settings_data["logLevel"] = log_level
current_session.active_tabs[tab_id] = tab_info
# 保存設定到檔案
with open(settings_file, "w", encoding="utf-8") as f:
json.dump(settings_data, f, ensure_ascii=False, indent=2)
# 同時更新全局標籤頁狀態
manager.global_active_tabs[tab_id] = tab_info
debug_log(f"標籤頁已註冊: {tab_id}")
debug_log(f"日誌等級已設定為: {log_level}")
return JSONResponse(
content={"status": "success", "tabId": tab_id, "registered": True}
content={
"status": "success",
"logLevel": log_level,
"messageCode": get_msg_code("log_level_updated"),
}
)
except Exception as e:
debug_log(f"註冊標籤頁失敗: {e}")
return JSONResponse(status_code=500, content={"error": f"註冊失敗: {e!s}"})
debug_log(f"設定日誌等級失敗: {e}")
return JSONResponse(
status_code=500,
content={
"status": "error",
"message": f"Set failed: {e!s}",
"messageCode": get_msg_code("set_failed"),
},
)
async def handle_websocket_message(manager: "WebUIManager", session, data: dict):
@ -494,20 +643,10 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict)
debug_log(f"發送狀態更新失敗: {e}")
elif message_type == "heartbeat":
# WebSocket 心跳處理
tab_id = data.get("tabId", "unknown")
timestamp = data.get("timestamp", 0)
tab_info = {"timestamp": timestamp, "last_seen": time.time()}
# 更新會話的標籤頁信息
if hasattr(session, "active_tabs"):
session.active_tabs[tab_id] = tab_info
else:
session.active_tabs = {tab_id: tab_info}
# 同時更新全局標籤頁狀態
manager.global_active_tabs[tab_id] = tab_info
# WebSocket 心跳處理(簡化版)
# 更新心跳時間
session.last_heartbeat = time.time()
session.last_activity = time.time()
# 發送心跳回應
if session.websocket:
@ -515,8 +654,7 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict)
await session.websocket.send_json(
{
"type": "heartbeat_response",
"tabId": tab_id,
"timestamp": timestamp,
"timestamp": data.get("timestamp", 0),
}
)
except Exception as e:
@ -529,6 +667,22 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict)
await session._cleanup_resources_on_timeout()
# 重構:不再自動停止服務器,保持服務器運行以支援持久性
elif message_type == "pong":
# 處理來自前端的 pong 回應(用於連接檢測)
debug_log(f"收到 pong 回應,時間戳: {data.get('timestamp', 'N/A')}")
# 可以在這裡記錄延遲或更新連接狀態
elif message_type == "update_timeout_settings":
# 處理超時設定更新
settings = data.get("settings", {})
debug_log(f"收到超時設定更新: {settings}")
if settings.get("enabled"):
session.update_timeout_settings(
enabled=True, timeout_seconds=settings.get("seconds", 3600)
)
else:
session.update_timeout_settings(enabled=False)
else:
debug_log(f"未知的消息類型: {message_type}")

View File

@ -8,44 +8,15 @@
/* ===== 音效管理區塊樣式 ===== */
.audio-management-section {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
transition: all 0.3s ease;
}
.audio-management-section:hover {
border-color: var(--accent-color);
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.1);
}
.audio-management-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.audio-management-title {
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
/* 音效描述文字 */
.audio-management-description {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 20px;
line-height: 1.4;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
/* ===== 音效設定控制項樣式 ===== */

View File

@ -0,0 +1,152 @@
/**
* MCP Feedback Enhanced - 瀏覽器通知設定樣式
* ========================================
*/
/* 權限狀態顯示 */
.permission-status {
font-size: 12px;
margin-top: 8px;
display: block; /* 使用 block 讓內容自然流動 */
}
.permission-status span {
display: inline-block; /* 確保內容在同一行 */
white-space: nowrap; /* 防止文字換行 */
line-height: 1.2; /* 適當的行高 */
}
/* 權限狀態樣式 */
.status-granted {
color: #059669;
}
.status-denied {
color: #dc2626;
}
.status-default {
color: #6366f1;
}
.status-unsupported {
color: #d97706;
}
/* 觸發情境選項 */
.notification-trigger {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.trigger-options {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 10px;
}
.radio-option {
display: flex;
align-items: center;
padding: 8px 12px;
background-color: var(--bg-secondary, #f9fafb);
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.radio-option:hover {
background-color: var(--bg-hover, #f3f4f6);
}
.radio-option input[type="radio"] {
margin-right: 10px;
cursor: pointer;
}
.radio-option span {
font-size: 14px;
color: var(--text-primary, #1f2937);
line-height: 1.4;
}
/* 測試按鈕區塊 */
.notification-actions {
margin-top: 16px;
}
.notification-actions .btn-primary {
padding: 8px 16px;
font-size: 14px;
background-color: var(--primary-color, #007acc);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.notification-actions .btn-primary:hover {
background-color: var(--primary-hover, #0066b3);
}
/* 通知設定專用的 setting-info 調整 */
#notificationSettings .setting-info {
max-width: calc(100% - 100px); /* 為開關預留空間 */
}
/* 響應式設計 */
@media (max-width: 768px) {
.permission-status {
font-size: 11px;
}
#notificationSettings .setting-info {
max-width: calc(100% - 80px); /* 小螢幕上調整空間 */
}
.radio-option {
padding: 6px 10px;
}
.radio-option span {
font-size: 13px;
}
}
/* 深色模式支援 */
@media (prefers-color-scheme: dark) {
.status-granted {
color: #34d399;
}
.status-denied {
color: #f87171;
}
.status-default {
color: #a5b4fc;
}
.status-unsupported {
color: #fbbf24;
}
.notification-trigger {
border-top-color: rgba(255, 255, 255, 0.1);
}
.radio-option {
background-color: rgba(255, 255, 255, 0.05);
}
.radio-option:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.radio-option span {
color: #e5e7eb;
}
}

View File

@ -51,6 +51,123 @@
gap: 20px;
}
/* 緊湊版頂部欄 */
.connection-monitor-bar.compact {
padding: 4px 12px;
gap: 0;
font-size: 12px;
height: 32px;
align-items: center;
justify-content: flex-start;
}
/* 緊湊版元素樣式 */
.app-title-compact {
font-weight: 600;
color: var(--text-primary);
font-size: 13px;
white-space: nowrap;
}
.info-separator {
margin: 0 6px;
color: var(--text-secondary);
opacity: 0.4;
font-size: 10px;
}
.project-info-compact,
.session-info-compact,
.countdown-display-compact,
.connection-status-compact {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
font-size: 11px;
}
.project-info-compact .project-path-display {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11px;
}
.session-info-compact .session-id-display {
font-size: 11px;
padding: 1px 4px;
}
.countdown-display-compact {
color: var(--warning-color);
}
.countdown-display-compact .countdown-timer {
font-weight: bold;
font-size: 12px;
}
/* 倒數計時器控制按鈕 */
.countdown-control-btn {
background: transparent;
border: none;
padding: 2px 6px;
margin-left: 4px;
cursor: pointer;
font-size: 12px;
color: var(--warning-color);
transition: all 0.2s ease;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
}
.countdown-control-btn:hover {
background: rgba(255, 152, 0, 0.2);
}
.countdown-control-btn:active {
transform: scale(0.95);
}
/* 暫停狀態的計時器樣式 */
.countdown-display-compact.paused .countdown-timer {
opacity: 0.6;
animation: pause-blink 1.5s ease-in-out infinite;
}
@keyframes pause-blink {
0%, 100% { opacity: 0.6; }
50% { opacity: 0.3; }
}
.connection-status-compact {
margin-left: auto;
}
.connection-status-compact .status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.connection-status-compact.connected {
color: var(--status-connected);
}
.connection-status-compact.disconnected {
color: var(--status-disconnected);
}
.connection-status-compact.connecting {
color: var(--status-connecting);
}
/* 應用資訊區域 */
.app-info-section {
display: flex;
@ -648,42 +765,37 @@
/* ===== 響應式設計 ===== */
@media (max-width: 768px) {
.connection-monitor-bar {
flex-direction: column;
gap: 8px;
padding: 12px 16px;
/* 緊湊版頂部欄在移動設備上的調整 */
.connection-monitor-bar.compact {
padding: 4px 10px;
font-size: 11px;
overflow-x: auto;
white-space: nowrap;
}
.app-info-section {
width: 100%;
text-align: center;
/* 隱藏專案路徑以節省空間 */
.project-info-compact {
display: none;
}
.app-title {
justify-content: center;
flex-wrap: wrap;
/* 隱藏相關的分隔符 */
.project-info-compact + .info-separator {
display: none;
}
.app-title h1 {
font-size: 16px;
/* 調整標題字體 */
.app-title-compact {
font-size: 12px;
}
.connection-status-group {
width: 100%;
justify-content: center;
flex-wrap: wrap;
/* 減少分隔符間距 */
.info-separator {
margin: 0 6px;
}
.connection-status-combined {
width: 100%;
align-items: center;
}
.detailed-status-info {
margin-left: 0;
margin-top: 0;
justify-content: center;
flex-wrap: wrap;
/* 確保倒數計時器始終可見 */
.countdown-display-compact {
font-weight: bold;
}
/* 會話管理頁籤響應式調整 */
@ -697,6 +809,28 @@
}
}
/* 超小螢幕調整 */
@media (max-width: 480px) {
/* 隱藏會話 ID */
.session-info-compact {
display: none;
}
/* 隱藏相關的分隔符 */
.session-info-compact + .info-separator {
display: none;
}
/* 保留核心資訊:標題、倒數計時器、連線狀態 */
.connection-monitor-bar.compact {
justify-content: space-between;
}
.connection-status-compact {
margin-left: 0;
}
}
/* ===== 載入狀態 ===== */
.loading-skeleton {
background: linear-gradient(90deg,
@ -724,6 +858,65 @@
outline-offset: 2px;
}
/* ===== 會話歷史區塊標題樣式 ===== */
.session-history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.session-history-header h4 {
margin: 0;
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
}
.session-history-actions {
display: flex;
gap: 8px;
align-items: center;
}
/* 響應式設計 - 小螢幕上的調整 */
@media (max-width: 768px) {
.session-history-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.session-history-actions {
width: 100%;
flex-wrap: wrap;
justify-content: flex-start;
}
.session-history-actions .btn-small {
font-size: 11px;
padding: 4px 8px;
}
}
/* 超小螢幕上使用下拉選單替代 */
@media (max-width: 480px) {
.session-history-actions {
position: relative;
}
.session-history-actions .btn-small:not(:first-child) {
display: none;
}
/* 可選:將第一個按鈕改為下拉選單觸發器 */
.session-history-actions .btn-small:first-child::after {
content: ' ▼';
font-size: 10px;
margin-left: 4px;
}
}
/* ===== 會話詳情彈窗 ===== */
.session-details-modal {
position: fixed;
@ -753,7 +946,7 @@
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 500px;
max-width: 750px;
width: 90%;
max-height: 80vh;
overflow: hidden;
@ -854,11 +1047,149 @@
.detail-value.summary {
background: var(--bg-secondary);
padding: 8px 12px;
padding: 0;
border-radius: 6px;
border-left: 3px solid var(--accent-color);
line-height: 1.4;
margin-top: 4px;
position: relative;
}
.summary-actions {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
}
.btn-copy-summary {
background: var(--accent-color);
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast) ease;
opacity: 0.8;
}
.btn-copy-summary:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.05);
}
.summary-content {
padding: 8px 12px;
padding-right: 50px; /* 为复制按钮留出空间 */
}
/* 为会话详情弹窗中的摘要内容应用与反馈页面相同的 Markdown 样式 */
.detail-value.summary .summary-content h1,
.detail-value.summary .summary-content h2,
.detail-value.summary .summary-content h3,
.detail-value.summary .summary-content h4,
.detail-value.summary .summary-content h5,
.detail-value.summary .summary-content h6 {
color: var(--text-primary);
margin: 16px 0 8px 0;
font-weight: 600;
}
.detail-value.summary .summary-content h1 { font-size: 20px; }
.detail-value.summary .summary-content h2 { font-size: 18px; }
.detail-value.summary .summary-content h3 { font-size: 16px; }
.detail-value.summary .summary-content h4 { font-size: 14px; }
.detail-value.summary .summary-content h5 { font-size: 13px; }
.detail-value.summary .summary-content h6 { font-size: 12px; }
.detail-value.summary .summary-content p {
margin: 8px 0;
line-height: 1.6;
}
.detail-value.summary .summary-content strong {
font-weight: 600;
color: var(--text-primary);
}
.detail-value.summary .summary-content em {
font-style: italic;
color: var(--text-primary);
}
.detail-value.summary .summary-content code {
background: var(--bg-tertiary);
padding: 2px 4px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
color: var(--accent-color);
}
.detail-value.summary .summary-content pre {
background: var(--bg-tertiary);
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
border-left: 3px solid var(--accent-color);
}
.detail-value.summary .summary-content pre code {
background: none;
padding: 0;
color: var(--text-primary);
}
.detail-value.summary .summary-content ul,
.detail-value.summary .summary-content ol {
margin: 8px 0;
padding-left: 20px;
}
.detail-value.summary .summary-content li {
margin: 4px 0;
line-height: 1.5;
}
.detail-value.summary .summary-content blockquote {
border-left: 4px solid var(--accent-color);
margin: 12px 0;
padding: 8px 16px;
background: rgba(0, 122, 204, 0.05);
font-style: italic;
}
/* 复制提示样式 */
.copy-toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: white;
z-index: 10000;
opacity: 0;
transform: translateX(100px);
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.copy-toast.show {
opacity: 1;
transform: translateX(0);
}
.copy-toast-success {
background: var(--success-color);
}
.copy-toast-error {
background: var(--error-color);
}
.modal-footer {
@ -924,6 +1255,36 @@
background: var(--bg-secondary);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.message-header .btn-copy-message {
background: var(--accent-color);
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
cursor: pointer;
transition: all var(--transition-fast) ease;
opacity: 0.7;
margin-left: 8px;
}
.message-header .btn-copy-message:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.05);
}
.user-message-item:hover .btn-copy-message {
opacity: 0.9;
}
.message-header {
display: flex;
justify-content: space-between;
@ -1036,3 +1397,32 @@
transition-duration: 0.01ms !important;
}
}
/* 響應式設計 - 緊湊版頂部欄 */
@media (max-width: 768px) {
/* 小螢幕隱藏專案路徑 */
.connection-monitor-bar.compact .project-info-compact {
display: none;
}
/* 隱藏相鄰的分隔符 */
.connection-monitor-bar.compact .project-info-compact + .info-separator {
display: none;
}
}
@media (max-width: 480px) {
/* 極小螢幕只顯示最關鍵資訊 */
.connection-monitor-bar.compact .session-info-compact {
display: none;
}
.connection-monitor-bar.compact .session-info-compact + .info-separator {
display: none;
}
/* 調整標題字體大小 */
.app-title-compact {
font-size: 12px;
}
}

View File

@ -98,6 +98,114 @@
z-index: 1000;
}
/* ===== 可折疊統計面板 ===== */
.stats-panel-floating {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 100;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
/* 收起狀態 */
.stats-panel-floating.collapsed {
transform: translateY(calc(100% - 40px));
}
/* 統計面板頭部 */
.stats-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
cursor: pointer;
user-select: none;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.stats-panel-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.stats-toggle-icon {
transition: transform 0.3s ease;
}
.stats-panel-floating.collapsed .stats-toggle-icon {
transform: rotate(180deg);
}
/* 統計面板內容 */
.stats-panel-content {
padding: 16px 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
max-width: 1200px;
margin: 0 auto;
}
/* 統計項目 */
.stats-item-detailed {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
background: var(--bg-primary);
border-radius: 6px;
border: 1px solid var(--border-color);
}
.stats-item-label {
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stats-item-value {
font-size: 16px;
font-weight: 600;
color: var(--accent-color);
font-family: 'Consolas', 'Monaco', monospace;
}
/* 動畫效能優化 */
@media (prefers-reduced-motion: reduce) {
.stats-panel-floating {
transition: none;
}
.stats-toggle-icon {
transition: none;
}
}
/* 響應式設計 */
@media (max-width: 768px) {
.stats-panel-content {
grid-template-columns: 1fr;
gap: 12px;
padding: 12px 16px;
}
.stats-item-detailed {
padding: 10px;
}
}
.tooltip:hover::after {
opacity: 1;
}
@ -369,10 +477,9 @@ body {
/* 容器樣式 */
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;
padding: 12px;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
@ -951,7 +1058,7 @@ body {
flex-direction: column;
min-width: 0;
min-height: 0; /* 允許內容自然收縮 */
padding: 20px;
padding: 12px;
overflow: hidden;
transition: all 0.6s cubic-bezier(0.4, 0.0, 0.2, 1);
background: var(--bg-primary);
@ -969,7 +1076,7 @@ body {
/* 分頁樣式 */
.tabs {
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
margin-bottom: 12px;
}
.tab-buttons {
@ -981,7 +1088,7 @@ body {
background: transparent;
border: none;
color: var(--text-secondary);
padding: 12px 20px;
padding: 10px 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
@ -1380,7 +1487,6 @@ h3.combined-section-title {
/* Placeholder 文本換行修復 - 全局樣式 */
textarea::placeholder,
#feedbackText::placeholder,
#combinedFeedbackText::placeholder {
white-space: pre-line !important;
line-height: 1.4 !important;
@ -1391,7 +1497,6 @@ textarea::placeholder,
/* 跨瀏覽器兼容的 placeholder 樣式 */
textarea::-webkit-input-placeholder,
#feedbackText::-webkit-input-placeholder,
#combinedFeedbackText::-webkit-input-placeholder {
white-space: pre-line !important;
line-height: 1.4 !important;
@ -1401,7 +1506,6 @@ textarea::-webkit-input-placeholder,
}
textarea::-moz-placeholder,
#feedbackText::-moz-placeholder,
#combinedFeedbackText::-moz-placeholder {
white-space: pre-line !important;
line-height: 1.4 !important;
@ -1411,7 +1515,6 @@ textarea::-moz-placeholder,
}
textarea:-ms-input-placeholder,
#feedbackText:-ms-input-placeholder,
#combinedFeedbackText:-ms-input-placeholder {
white-space: pre-line !important;
line-height: 1.4 !important;
@ -1908,3 +2011,193 @@ textarea:-ms-input-placeholder,
margin-bottom: 0 !important;
}
/* 會話超時倒數樣式 */
.session-timeout-display {
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 4px;
padding: 2px 8px;
}
.session-timeout-display .timeout-label {
font-size: 12px;
color: var(--text-secondary);
margin-right: 4px;
}
.session-timeout-display .countdown-timer {
color: var(--text-primary);
}
.session-timeout-display.countdown-warning .countdown-timer {
color: var(--error-color);
animation: pulse 1s ease-in-out infinite;
}
/* 自動提交倒數樣式 */
.auto-submit-display {
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
border-radius: 4px;
padding: 2px 8px;
}
.auto-submit-display .countdown-label {
font-size: 12px;
color: var(--text-secondary);
margin-right: 4px;
}
.auto-submit-display .countdown-timer {
color: var(--text-primary);
font-weight: 500;
}
/* 倒數控制按鈕 */
.countdown-control-btn {
background: transparent;
border: none;
padding: 2px 6px;
margin-left: 4px;
cursor: pointer;
font-size: 14px;
color: var(--text-secondary);
transition: color 0.2s ease;
}
.countdown-control-btn:hover {
color: var(--text-primary);
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
/* 自動執行命令設定樣式 */
.command-auto-settings {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.command-auto-settings .settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.command-auto-settings .settings-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.command-auto-settings .settings-content {
transition: opacity 0.3s ease;
}
.command-auto-settings .settings-content.disabled {
opacity: 0.5;
pointer-events: none;
}
.auto-command-item {
margin-bottom: 20px;
padding: 12px;
background: var(--bg-tertiary);
border-radius: 6px;
border: 1px solid var(--border-color);
}
.auto-command-item:last-child {
margin-bottom: 0;
}
.command-label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-primary);
}
.command-icon {
font-size: 18px;
}
.command-input-wrapper {
display: flex;
align-items: center;
margin: 8px 0;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0 12px;
transition: border-color 0.3s ease;
}
.command-input-wrapper:focus-within {
border-color: var(--accent-color);
}
.command-prefix {
color: var(--accent-color);
font-weight: bold;
margin-right: 8px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
.auto-command-input {
flex: 1;
border: none;
background: transparent;
padding: 8px 0;
color: var(--text-primary);
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
outline: none;
}
.command-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.btn-test {
background: var(--info-color);
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background 0.3s ease;
}
.btn-test:hover {
background: #1976d2;
}
.command-hint {
color: var(--text-secondary);
font-size: 12px;
}
.btn-small {
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
<!-- 背景圆形 -->
<circle cx="16" cy="16" r="15" fill="#2563eb" stroke="#1e40af" stroke-width="2"/>
<!-- MCP 字母 M -->
<path d="M6 10 L6 22 L8 22 L8 14 L10 18 L12 14 L12 22 L14 22 L14 10 L11 10 L10 14 L9 10 Z" fill="white"/>
<!-- 反馈图标 - 对话气泡 -->
<path d="M18 8 C20.2 8 22 9.8 22 12 L22 16 C22 18.2 20.2 20 18 20 L16 20 L14 22 L14 20 L16 20 C16 20 18 20 18 20 C19.1 20 20 19.1 20 18 L20 14 C20 12.9 19.1 12 18 12 L16 12 C14.9 12 14 12.9 14 14 L14 16 C14 17.1 14.9 18 16 18" fill="none" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<!-- 反馈箭头 -->
<path d="M24 14 L26 16 L24 18" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- 小点表示活跃状态 -->
<circle cx="19" cy="15" r="1" fill="#10b981"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

File diff suppressed because it is too large Load Diff

View File

@ -8,20 +8,43 @@
class I18nManager {
constructor() {
this.currentLanguage = 'zh-TW';
this.currentLanguage = this.getDefaultLanguage();
this.translations = {};
this.loadingPromise = null;
}
getDefaultLanguage() {
// 1. 先檢查本地儲存的設定
const savedLanguage = localStorage.getItem('language');
if (savedLanguage && ['zh-TW', 'zh-CN', 'en'].includes(savedLanguage)) {
console.log('🌐 使用儲存的語言設定:', savedLanguage);
return savedLanguage;
}
// 2. 檢查瀏覽器語言
const browserLang = navigator.language || navigator.userLanguage;
console.log('🌐 瀏覽器語言:', browserLang);
if (browserLang.startsWith('zh-TW') || browserLang.includes('Hant')) {
console.log('🌐 偵測到繁體中文環境');
return 'zh-TW';
}
if (browserLang.startsWith('zh') || browserLang.includes('Hans')) {
console.log('🌐 偵測到簡體中文環境');
return 'zh-CN';
}
if (browserLang.startsWith('en')) {
console.log('🌐 偵測到英文環境');
return 'en';
}
// 3. 預設使用繁體中文
console.log('🌐 使用預設語言: zh-TW');
return 'zh-TW';
}
async init() {
// 從 localStorage 載入語言偏好
const savedLanguage = localStorage.getItem('language');
if (savedLanguage) {
this.currentLanguage = savedLanguage;
console.log(`i18nManager 從 localStorage 載入語言: ${savedLanguage}`);
} else {
console.log(`i18nManager 使用默認語言: ${this.currentLanguage}`);
}
console.log(`i18nManager 使用預設語言: ${this.currentLanguage}`);
// 載入翻譯數據
await this.loadTranslations();
@ -125,7 +148,6 @@ class I18nManager {
console.log(`🔄 i18nManager.setLanguage() 被調用: ${this.currentLanguage} -> ${language}`);
if (this.translations[language]) {
this.currentLanguage = language;
localStorage.setItem('language', language);
this.applyTranslations();
// 更新所有語言選擇器(包括現代化版本)
@ -171,6 +193,16 @@ class I18nManager {
}
});
// 翻譯有 data-i18n-aria-label 屬性的元素
const ariaLabelElements = document.querySelectorAll('[data-i18n-aria-label]');
ariaLabelElements.forEach(element => {
const key = element.getAttribute('data-i18n-aria-label');
const translation = this.t(key);
if (translation && translation !== key) {
element.setAttribute('aria-label', translation);
}
});
// 更新動態內容
this.updateDynamicContent();
@ -235,6 +267,11 @@ class I18nManager {
const stats = window.feedbackApp.sessionManager.dataManager.getStats();
window.feedbackApp.sessionManager.uiRenderer.renderStats(stats);
console.log('🌐 已更新統計資訊的語言顯示');
// 重新渲染會話歷史以更新所有動態創建的元素
const sessionHistory = window.feedbackApp.sessionManager.dataManager.getSessionHistory();
window.feedbackApp.sessionManager.uiRenderer.renderSessionHistory(sessionHistory);
console.log('🌐 已更新會話歷史的語言顯示');
}
}
@ -293,27 +330,18 @@ class I18nManager {
// 設定頁籤的下拉選擇器
const selector = document.getElementById('settingsLanguageSelect');
if (selector) {
// 設置當前值
// 設置當前值,不綁定事件(讓 SettingsManager 統一處理)
selector.value = this.currentLanguage;
console.log(`🔧 setupLanguageSelectors: 設置 select.value = ${this.currentLanguage}`);
// 移除舊的事件監聽器(如果存在)
if (selector._i18nChangeHandler) {
selector.removeEventListener('change', selector._i18nChangeHandler);
}
// 添加新的事件監聽器
selector._i18nChangeHandler = (e) => {
console.log(`🔄 i18n select change event: ${e.target.value}`);
this.setLanguage(e.target.value);
};
selector.addEventListener('change', selector._i18nChangeHandler);
// 不再綁定事件監聽器,避免與 SettingsManager 衝突
// 事件處理完全交由 SettingsManager 負責
}
// 新版現代化語言選擇器
const languageOptions = document.querySelectorAll('.language-option');
if (languageOptions.length > 0) {
// 設置當前語言的活躍狀態和點擊事件
// 只設置當前語言的活躍狀態,不綁定事件
languageOptions.forEach(option => {
const lang = option.getAttribute('data-lang');
if (lang === this.currentLanguage) {
@ -321,16 +349,8 @@ class I18nManager {
} else {
option.classList.remove('active');
}
// 移除舊的事件監聽器(如果存在)
option.removeEventListener('click', option._languageClickHandler);
// 添加新的點擊事件監聽器
option._languageClickHandler = () => {
this.setLanguage(lang);
};
option.addEventListener('click', option._languageClickHandler);
});
// 事件監聽器由 SettingsManager 統一處理,避免重複綁定
}
}

View File

@ -591,7 +591,7 @@
// 可以在這裡添加 UI 通知邏輯
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
const message = window.i18nManager ?
window.i18nManager.t('audio.autoplayBlocked', '瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知') :
window.i18nManager.t('notification.autoplayBlocked', '瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知') :
'瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知';
window.MCPFeedback.Utils.showMessage(message, 'info');
}

View File

@ -70,27 +70,26 @@
*/
AudioSettingsUI.prototype.createUI = function() {
const html = `
<div class="audio-management-section">
<div class="audio-management-header">
<h4 class="audio-management-title" data-i18n="audio.notification.title">
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="audio.notification.title">
🔊 音效通知設定
</h4>
</h3>
</div>
<div class="audio-management-description" data-i18n="audio.notification.description">
設定會話更新時的音效通知
</div>
<div class="audio-settings-controls">
<div class="settings-card-body">
<div class="audio-management-description" data-i18n="audio.notification.description">
設定會話更新時的音效通知
</div>
<div class="audio-settings-controls">
<!-- 啟用開關 -->
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="audio.notification.enabled">啟用音效通知</div>
<div class="setting-description" data-i18n="audio.notification.enabledDesc">
啟用後將在有新會話更新時播放音效通知
</div>
<div class="setting-label" data-i18n="audio.notification.enabled"></div>
<div class="setting-description" data-i18n="audio.notification.enabledDesc"></div>
</div>
<div class="setting-control">
<button type="button" id="audioNotificationEnabled" class="toggle-btn" aria-label="切換音效通知">
<button type="button" id="audioNotificationEnabled" class="toggle-btn" data-i18n-aria-label="aria.toggleAudioNotification">
<span class="toggle-slider"></span>
</button>
</div>
@ -142,6 +141,7 @@
</div>
</div>
</div>
</div>
</div>
`;
@ -666,6 +666,16 @@
}
});
// 對有 data-i18n-aria-label 屬性的元素應用翻譯
const ariaLabelElements = this.container.querySelectorAll('[data-i18n-aria-label]');
ariaLabelElements.forEach(element => {
const key = element.getAttribute('data-i18n-aria-label');
const translation = this.t(key);
if (translation && translation !== key) {
element.setAttribute('aria-label', translation);
}
});
console.log('🌐 AudioSettingsUI 初始翻譯已應用');
};

View File

@ -133,6 +133,36 @@
indicator.className = 'connection-indicator ' + status;
}
// 更新精簡的頂部狀態指示器(現在是緊湊版)
const minimalIndicator = document.getElementById('connectionStatusMinimal');
if (minimalIndicator) {
minimalIndicator.className = 'connection-status-compact ' + status;
const statusText = minimalIndicator.querySelector('.status-text');
if (statusText) {
let statusKey = '';
switch (status) {
case 'connected':
statusKey = 'connectionMonitor.connected';
break;
case 'connecting':
statusKey = 'connectionMonitor.connecting';
break;
case 'disconnected':
statusKey = 'connectionMonitor.disconnected';
break;
case 'reconnecting':
statusKey = 'connectionMonitor.reconnecting';
break;
default:
statusKey = 'connectionMonitor.unknown';
}
statusText.setAttribute('data-i18n', statusKey);
if (window.i18nManager) {
statusText.textContent = window.i18nManager.t(statusKey);
}
}
}
// 處理特殊狀態
switch (status) {
case 'connected':
@ -282,15 +312,30 @@
}
}
// 更新統計面板中的延遲顯示
const statsLatency = document.getElementById('statsLatency');
if (statsLatency) {
statsLatency.textContent = this.currentLatency > 0 ? this.currentLatency + 'ms' : '--ms';
}
// 更新連線時間
if (this.connectionTimeDisplay && this.connectionStartTime) {
let connectionTimeStr = '--:--';
if (this.connectionStartTime) {
const duration = Math.floor((Date.now() - this.connectionStartTime) / 1000);
const minutes = Math.floor(duration / 60);
const seconds = duration % 60;
connectionTimeStr = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
}
if (this.connectionTimeDisplay) {
const connectionTimeLabel = window.i18nManager ? window.i18nManager.t('connectionMonitor.connectionTime') : '連線時間';
this.connectionTimeDisplay.textContent = connectionTimeLabel + ': ' +
String(minutes).padStart(2, '0') + ':' +
String(seconds).padStart(2, '0');
this.connectionTimeDisplay.textContent = connectionTimeLabel + ': ' + connectionTimeStr;
}
// 更新統計面板中的連線時間
const statsConnectionTime = document.getElementById('statsConnectionTime');
if (statsConnectionTime) {
statsConnectionTime.textContent = connectionTimeStr;
}
// 更新重連次數
@ -300,10 +345,35 @@
this.reconnectCountDisplay.textContent = reconnectLabel + ': ' + this.reconnectCount + ' ' + timesLabel;
}
// 更新統計面板中的重連次數
const statsReconnectCount = document.getElementById('statsReconnectCount');
if (statsReconnectCount) {
statsReconnectCount.textContent = this.reconnectCount.toString();
}
// 更新訊息計數
if (this.messageCountDisplay) {
this.messageCountDisplay.textContent = this.messageCount;
}
// 更新統計面板中的訊息計數
const statsMessageCount = document.getElementById('statsMessageCount');
if (statsMessageCount) {
statsMessageCount.textContent = this.messageCount.toString();
}
// 更新統計面板中的會話數和狀態
const sessionCount = document.getElementById('sessionCount');
const statsSessionCount = document.getElementById('statsSessionCount');
if (sessionCount && statsSessionCount) {
statsSessionCount.textContent = sessionCount.textContent;
}
const sessionStatusText = document.getElementById('sessionStatusText');
const statsSessionStatus = document.getElementById('statsSessionStatus');
if (sessionStatusText && statsSessionStatus) {
statsSessionStatus.textContent = sessionStatusText.textContent;
}
};
/**

View File

@ -0,0 +1,168 @@
/**
* MCP Feedback Enhanced - 訊息代碼常量
* ====================================
*
* 定義所有系統訊息的標準代碼用於國際化支援
*/
(function() {
'use strict';
// 確保命名空間存在
window.MCPFeedback = window.MCPFeedback || {};
window.MCPFeedback.Constants = window.MCPFeedback.Constants || {};
/**
* 訊息代碼枚舉
* 所有系統訊息都應該使用這些代碼而非硬編碼字串
*/
const MessageCodes = {
// 系統狀態訊息
SYSTEM: {
CONNECTION_ESTABLISHED: 'system.connectionEstablished',
CONNECTION_LOST: 'system.connectionLost',
CONNECTION_RECONNECTING: 'system.connectionReconnecting',
CONNECTION_RECONNECTED: 'system.connectionReconnected',
CONNECTION_FAILED: 'system.connectionFailed',
WEBSOCKET_ERROR: 'system.websocketError'
},
// 會話相關訊息
SESSION: {
NO_ACTIVE_SESSION: 'session.noActiveSession',
SESSION_CREATED: 'session.created',
SESSION_UPDATED: 'session.updated',
SESSION_EXPIRED: 'session.expired',
SESSION_TIMEOUT: 'session.timeout',
SESSION_CLEANED: 'session.cleaned',
FEEDBACK_SUBMITTED: 'session.feedbackSubmitted',
USER_MESSAGE_RECORDED: 'session.userMessageRecorded',
HISTORY_SAVED: 'session.historySaved',
HISTORY_LOADED: 'session.historyLoaded',
MANUAL_CLEANUP: 'session.manualCleanup',
ERROR_CLEANUP: 'session.errorCleanup'
},
// 設定相關訊息
SETTINGS: {
SAVED: 'settings.saved',
LOADED: 'settings.loaded',
CLEARED: 'settings.cleared',
SAVE_FAILED: 'settings.saveFailed',
LOAD_FAILED: 'settings.loadFailed',
CLEAR_FAILED: 'settings.clearFailed',
INVALID_VALUE: 'settings.invalidValue',
LOG_LEVEL_UPDATED: 'settings.logLevelUpdated',
INVALID_LOG_LEVEL: 'settings.invalidLogLevel'
},
// 通知相關訊息
NOTIFICATION: {
AUTOPLAY_BLOCKED: 'notification.autoplayBlocked',
PERMISSION_DENIED: 'notification.permissionDenied',
PERMISSION_GRANTED: 'notification.permissionGranted',
TEST_SENT: 'notification.testSent',
SOUND_ENABLED: 'notification.soundEnabled',
SOUND_DISABLED: 'notification.soundDisabled'
},
// 檔案上傳訊息
FILE: {
UPLOAD_SUCCESS: 'file.uploadSuccess',
UPLOAD_FAILED: 'file.uploadFailed',
SIZE_TOO_LARGE: 'file.sizeTooLarge',
TYPE_NOT_SUPPORTED: 'file.typeNotSupported',
PROCESSING: 'file.processing',
REMOVED: 'file.removed'
},
// 提示詞相關訊息
PROMPT: {
SAVED: 'prompt.saved',
DELETED: 'prompt.deleted',
APPLIED: 'prompt.applied',
IMPORT_SUCCESS: 'prompt.importSuccess',
IMPORT_FAILED: 'prompt.importFailed',
EXPORT_SUCCESS: 'prompt.exportSuccess',
VALIDATION_FAILED: 'prompt.validationFailed'
},
// 錯誤訊息
ERROR: {
GENERIC: 'error.generic',
NETWORK: 'error.network',
SERVER: 'error.server',
TIMEOUT: 'error.timeout',
INVALID_INPUT: 'error.invalidInput',
OPERATION_FAILED: 'error.operationFailed'
},
// 命令執行訊息
COMMAND: {
EXECUTING: 'commandStatus.executing',
COMPLETED: 'commandStatus.completed',
FAILED: 'commandStatus.failed',
OUTPUT_RECEIVED: 'commandStatus.outputReceived',
INVALID_COMMAND: 'commandStatus.invalid',
ERROR: 'commandStatus.error'
}
};
/**
* 訊息嚴重程度
*/
const MessageSeverity = {
INFO: 'info',
SUCCESS: 'success',
WARNING: 'warning',
ERROR: 'error'
};
/**
* 建立標準訊息物件
* @param {string} code - 訊息代碼
* @param {Object} params - 動態參數
* @param {string} severity - 嚴重程度
* @returns {Object} 標準訊息物件
*/
function createMessage(code, params = {}, severity = MessageSeverity.INFO) {
return {
type: 'notification',
code: code,
params: params,
severity: severity,
timestamp: Date.now()
};
}
/**
* 快捷方法建立成功訊息
*/
function createSuccessMessage(code, params = {}) {
return createMessage(code, params, MessageSeverity.SUCCESS);
}
/**
* 快捷方法建立錯誤訊息
*/
function createErrorMessage(code, params = {}) {
return createMessage(code, params, MessageSeverity.ERROR);
}
/**
* 快捷方法建立警告訊息
*/
function createWarningMessage(code, params = {}) {
return createMessage(code, params, MessageSeverity.WARNING);
}
// 匯出到全域命名空間
window.MCPFeedback.Constants.MessageCodes = MessageCodes;
window.MCPFeedback.Constants.MessageSeverity = MessageSeverity;
window.MCPFeedback.Constants.createMessage = createMessage;
window.MCPFeedback.Constants.createSuccessMessage = createSuccessMessage;
window.MCPFeedback.Constants.createErrorMessage = createErrorMessage;
window.MCPFeedback.Constants.createWarningMessage = createWarningMessage;
console.log('📋 訊息代碼常量載入完成');
})();

View File

@ -289,14 +289,23 @@
if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
const sizeLimit = this.formatFileSize(this.maxFileSize);
console.warn('⚠️ 檔案過大:', file.name, '超過限制', sizeLimit);
this.showMessage('圖片大小超過限制 (' + sizeLimit + '): ' + file.name, 'warning');
const message = window.i18nManager ?
window.i18nManager.t('fileUpload.fileSizeExceeded', {
limit: sizeLimit,
filename: file.name
}) :
'圖片大小超過限制 (' + sizeLimit + '): ' + file.name;
this.showMessage(message, 'warning');
continue;
}
// 檢查檔案數量限制
if (this.files.length + validFiles.length >= this.maxFiles) {
console.warn('⚠️ 檔案數量超過限制:', this.maxFiles);
this.showMessage('最多只能上傳 ' + this.maxFiles + ' 個檔案', 'warning');
const message = window.i18nManager ?
window.i18nManager.t('fileUpload.maxFilesExceeded', { maxFiles: this.maxFiles }) :
'最多只能上傳 ' + this.maxFiles + ' 個檔案';
this.showMessage(message, 'warning');
break;
}
@ -340,7 +349,10 @@
})
.catch(function(error) {
console.error('❌ 檔案處理失敗:', error);
self.showMessage('檔案處理失敗,請重試', 'error');
const message = window.i18nManager ?
window.i18nManager.t('fileUpload.processingFailed', '檔案處理失敗,請重試') :
'檔案處理失敗,請重試';
self.showMessage(message, 'error');
});
};

View File

@ -311,29 +311,94 @@
if (urlLogLevel) {
return urlLogLevel;
}
// 檢查 localStorage
const storedLevel = localStorage.getItem('mcp-log-level');
if (storedLevel) {
return storedLevel;
}
// 檢查是否為開發環境
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
return LogLevel.DEBUG;
}
return LogLevel.INFO;
}
// 從 API 載入日誌等級
function loadLogLevelFromAPI() {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/log-level?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
}
throw new Error('載入日誌等級失敗: ' + response.status);
})
.then(function(data) {
const apiLogLevel = data.logLevel;
if (apiLogLevel && Object.values(LogLevel).includes(apiLogLevel)) {
currentLogLevel = apiLogLevel;
console.log('📋 從 API 載入日誌等級:', apiLogLevel);
}
})
.catch(function(error) {
console.warn('⚠️ 載入日誌等級失敗,使用預設值:', error);
});
}
// 保存日誌等級到 API
function saveLogLevelToAPI(logLevel) {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/log-level?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
logLevel: logLevel
})
})
.then(function(response) {
if (response.ok) {
return response.json();
}
throw new Error('保存日誌等級失敗: ' + response.status);
})
.then(function(data) {
console.log('📋 日誌等級已保存:', data.logLevel);
// 處理訊息代碼
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode, data.params);
console.log('伺服器回應:', message);
}
})
.catch(function(error) {
console.warn('⚠️ 保存日誌等級失敗:', error);
});
}
// 設置全域日誌等級
globalLogger.setLevel(detectLogLevel());
// 頁面載入後從 API 載入日誌等級
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadLogLevelFromAPI);
} else {
loadLogLevelFromAPI();
}
// 匯出到全域命名空間
window.MCPFeedback.Logger = Logger;
window.MCPFeedback.LogLevel = LogLevel;
window.MCPFeedback.logger = globalLogger;
// 匯出設定方法
window.MCPFeedback.setLogLevel = function(logLevel) {
if (Object.values(LogLevel).includes(logLevel)) {
globalLogger.setLevel(logLevel);
saveLogLevelToAPI(logLevel);
console.log('📋 日誌等級已更新:', LogLevelNames[logLevel]);
} else {
console.warn('⚠️ 無效的日誌等級:', logLevel);
}
};
console.log('✅ Logger 模組載入完成,當前等級:', LogLevelNames[globalLogger.getLevel()]);
})();

View File

@ -0,0 +1,360 @@
/**
* MCP Feedback Enhanced - 通知管理模組
* ===================================
*
* 處理瀏覽器通知功能支援新會話通知和緊急狀態通知
* 使用 Web Notification API提供極簡的通知體驗
*/
(function() {
'use strict';
// 確保命名空間存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* 通知管理器建構函數
*/
function NotificationManager(options) {
options = options || {};
// 通知設定
this.enabled = false;
this.permission = 'default';
this.triggerMode = 'focusLost'; // 預設為失去焦點時通知
// 狀態追蹤
this.lastSessionId = null; // 避免重複通知同一會話
this.isInitialized = false;
this.hasFocus = true; // 追蹤視窗焦點狀態
// 設定鍵名
this.STORAGE_KEY = 'notificationsEnabled';
this.TRIGGER_MODE_KEY = 'notificationTriggerMode';
// i18n 翻譯函數
this.t = options.t || function(key, defaultValue) { return defaultValue || key; };
console.log('🔔 NotificationManager 建構完成');
}
/**
* 初始化通知管理器
*/
NotificationManager.prototype.initialize = function() {
if (this.isInitialized) return;
// 檢查瀏覽器支援
if (!this.checkBrowserSupport()) {
console.warn('⚠️ 瀏覽器不支援 Notification API');
return;
}
// 載入設定
this.loadSettings();
// 更新權限狀態
this.updatePermissionStatus();
// 設定焦點追蹤
this.setupFocusTracking();
this.isInitialized = true;
console.log('✅ NotificationManager 初始化完成', {
enabled: this.enabled,
permission: this.permission,
triggerMode: this.triggerMode
});
};
/**
* 檢查瀏覽器支援
*/
NotificationManager.prototype.checkBrowserSupport = function() {
return 'Notification' in window;
};
/**
* 載入設定
*/
NotificationManager.prototype.loadSettings = function() {
try {
this.enabled = localStorage.getItem(this.STORAGE_KEY) === 'true';
this.triggerMode = localStorage.getItem(this.TRIGGER_MODE_KEY) || 'focusLost';
} catch (error) {
console.error('❌ 載入通知設定失敗:', error);
this.enabled = false;
this.triggerMode = 'focusLost';
}
};
/**
* 儲存設定
*/
NotificationManager.prototype.saveSettings = function() {
try {
localStorage.setItem(this.STORAGE_KEY, this.enabled.toString());
} catch (error) {
console.error('❌ 儲存通知設定失敗:', error);
}
};
/**
* 更新權限狀態
*/
NotificationManager.prototype.updatePermissionStatus = function() {
if (this.checkBrowserSupport()) {
this.permission = Notification.permission;
}
};
/**
* 請求通知權限
*/
NotificationManager.prototype.requestPermission = async function() {
if (!this.checkBrowserSupport()) {
throw new Error('瀏覽器不支援通知功能');
}
try {
const result = await Notification.requestPermission();
this.permission = result;
return result;
} catch (error) {
console.error('❌ 請求通知權限失敗:', error);
throw error;
}
};
/**
* 啟用通知
*/
NotificationManager.prototype.enable = async function() {
// 檢查權限
if (this.permission === 'default') {
const result = await this.requestPermission();
if (result !== 'granted') {
return false;
}
} else if (this.permission === 'denied') {
console.warn('⚠️ 通知權限已被拒絕');
return false;
}
this.enabled = true;
this.saveSettings();
console.log('✅ 通知已啟用');
return true;
};
/**
* 停用通知
*/
NotificationManager.prototype.disable = function() {
this.enabled = false;
this.saveSettings();
console.log('🔇 通知已停用');
};
/**
* 設定焦點追蹤
*/
NotificationManager.prototype.setupFocusTracking = function() {
const self = this;
// 監聽焦點事件
window.addEventListener('focus', function() {
self.hasFocus = true;
console.log('👁️ 視窗獲得焦點');
});
window.addEventListener('blur', function() {
self.hasFocus = false;
console.log('👁️ 視窗失去焦點');
});
};
/**
* 檢查是否可以顯示通知
*/
NotificationManager.prototype.canNotify = function() {
if (!this.enabled || this.permission !== 'granted') {
return false;
}
// 根據觸發模式判斷
switch (this.triggerMode) {
case 'always':
return true; // 總是通知
case 'background':
return document.hidden; // 只在頁面隱藏時通知
case 'tabSwitch':
return document.hidden; // 只在切換標籤頁時通知
case 'focusLost':
return document.hidden || !this.hasFocus; // 失去焦點或頁面隱藏時通知
default:
return document.hidden || !this.hasFocus;
}
};
/**
* 新會話通知
*/
NotificationManager.prototype.notifyNewSession = function(sessionId, projectPath) {
// 避免重複通知
if (sessionId === this.lastSessionId) {
console.log('🔇 跳過重複的會話通知');
return;
}
// 檢查是否可以通知
if (!this.canNotify()) {
console.log('🔇 不符合通知條件', {
enabled: this.enabled,
permission: this.permission,
pageHidden: document.hidden,
hasFocus: this.hasFocus,
triggerMode: this.triggerMode
});
return;
}
this.lastSessionId = sessionId;
try {
const notification = new Notification(this.t('notification.browser.title', 'MCP Feedback - 新會話'), {
body: `${this.t('notification.browser.ready', '準備就緒')}: ${this.truncatePath(projectPath)}`,
icon: '/static/icon-192.png',
badge: '/static/icon-192.png',
tag: 'mcp-session',
timestamp: Date.now(),
silent: false
});
// 點擊後聚焦視窗
notification.onclick = () => {
window.focus();
notification.close();
console.log('🖱️ 通知被點擊,視窗已聚焦');
};
// 5秒後自動關閉
setTimeout(() => notification.close(), 5000);
console.log('🔔 已發送新會話通知', {
sessionId: sessionId,
projectPath: projectPath
});
} catch (error) {
console.error('❌ 發送通知失敗:', error);
}
};
/**
* 緊急通知連線問題等
*/
NotificationManager.prototype.notifyCritical = function(type, message) {
if (!this.canNotify()) return;
try {
const notification = new Notification(this.t('notification.browser.criticalTitle', 'MCP Feedback - 警告'), {
body: message,
icon: '/static/icon-192.png',
badge: '/static/icon-192.png',
tag: 'mcp-critical',
requireInteraction: true, // 需要手動關閉
timestamp: Date.now()
});
notification.onclick = () => {
window.focus();
notification.close();
console.log('🖱️ 緊急通知被點擊');
};
console.log('⚠️ 已發送緊急通知', {
type: type,
message: message
});
} catch (error) {
console.error('❌ 發送緊急通知失敗:', error);
}
};
/**
* 路徑截斷顯示
*/
NotificationManager.prototype.truncatePath = function(path, maxLength) {
maxLength = maxLength || 50;
if (!path || path.length <= maxLength) return path || this.t('notification.browser.unknownProject', '未知專案');
return '...' + path.slice(-(maxLength - 3));
};
/**
* 設定觸發模式
*/
NotificationManager.prototype.setTriggerMode = function(mode) {
const validModes = ['always', 'background', 'tabSwitch', 'focusLost'];
if (validModes.includes(mode)) {
this.triggerMode = mode;
try {
localStorage.setItem(this.TRIGGER_MODE_KEY, mode);
console.log('✅ 通知觸發模式已更新:', mode);
} catch (error) {
console.error('❌ 儲存觸發模式失敗:', error);
}
}
};
/**
* 獲取當前設定
*/
NotificationManager.prototype.getSettings = function() {
return {
enabled: this.enabled,
permission: this.permission,
browserSupported: this.checkBrowserSupport(),
triggerMode: this.triggerMode
};
};
/**
* 測試通知
*/
NotificationManager.prototype.testNotification = function() {
if (!this.checkBrowserSupport()) {
alert(this.t('notification.browser.notSupported', '您的瀏覽器不支援通知功能'));
return;
}
if (this.permission !== 'granted') {
alert(this.t('notification.browser.permissionRequired', '請先授權通知權限'));
return;
}
try {
const notification = new Notification(this.t('notification.browser.testTitle', '測試通知'), {
body: this.t('notification.browser.testBody', '這是一個測試通知5秒後將自動關閉'),
icon: '/static/icon-192.png',
tag: 'mcp-test',
timestamp: Date.now()
});
notification.onclick = () => {
notification.close();
};
setTimeout(() => notification.close(), 5000);
console.log('🔔 測試通知已發送');
} catch (error) {
console.error('❌ 測試通知失敗:', error);
alert('發送測試通知失敗');
}
};
// 匯出到全域命名空間
window.MCPFeedback.NotificationManager = NotificationManager;
})();

View File

@ -0,0 +1,344 @@
/**
* MCP Feedback Enhanced - 通知設定介面模組
* =====================================
*
* 處理瀏覽器通知的設定介面提供簡單的開關控制
* NotificationManager 配合使用
*/
(function() {
'use strict';
// 確保命名空間存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* 通知設定介面建構函數
*/
function NotificationSettings(options) {
options = options || {};
// 容器元素
this.container = options.container || null;
// 通知管理器引用
this.notificationManager = options.notificationManager || null;
// i18n 翻譯函數
this.t = options.t || function(key, defaultValue) { return defaultValue || key; };
// UI 元素引用
this.toggle = null;
this.statusDiv = null;
this.testButton = null;
this.triggerOptionsDiv = null;
console.log('🎨 NotificationSettings 初始化完成');
}
/**
* 初始化設定介面
*/
NotificationSettings.prototype.initialize = function() {
if (!this.container) {
console.error('❌ NotificationSettings 容器未設定');
return;
}
if (!this.notificationManager) {
console.error('❌ NotificationManager 未設定');
return;
}
this.createUI();
this.setupEventListeners();
this.updateUI();
// 應用翻譯到動態生成的內容
if (window.i18nManager) {
window.i18nManager.applyTranslations();
}
console.log('✅ NotificationSettings 初始化完成');
};
/**
* 創建 UI 結構
*/
NotificationSettings.prototype.createUI = function() {
const html = `
<!-- 啟用開關 -->
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="notification.settingLabel"></div>
<div class="setting-description" data-i18n="notification.description"></div>
<!-- 權限狀態 -->
<div id="permissionStatus" class="permission-status">
<!-- 動態更新 -->
</div>
</div>
<div class="setting-control">
<button type="button" id="notificationToggle" class="toggle-btn" data-i18n-aria-label="aria.toggleNotification">
<span class="toggle-slider"></span>
</button>
</div>
</div>
<!-- 通知觸發情境 -->
<div class="setting-item notification-trigger" style="display: none;">
<div class="setting-info">
<div class="setting-label" data-i18n="notification.triggerTitle"></div>
<div class="setting-description" data-i18n="notification.triggerDescription"></div>
</div>
<div class="trigger-options">
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="focusLost" checked>
<span data-i18n="notification.trigger.focusLost"></span>
</label>
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="tabSwitch">
<span data-i18n="notification.trigger.tabSwitch"></span>
</label>
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="background">
<span data-i18n="notification.trigger.background"></span>
</label>
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="always">
<span data-i18n="notification.trigger.always"></span>
</label>
</div>
</div>
<!-- 測試按鈕 -->
<div class="setting-item notification-actions" style="display: none;">
<div class="setting-info">
<div class="setting-label" data-i18n="notification.testTitle"></div>
<div class="setting-description" data-i18n="notification.testDescription"></div>
</div>
<div class="setting-control">
<button type="button" id="testNotification" class="btn-primary">
<span data-i18n="notification.test"></span>
</button>
</div>
</div>
`;
this.container.innerHTML = html;
// 取得元素引用
this.toggle = this.container.querySelector('#notificationToggle');
this.statusDiv = this.container.querySelector('#permissionStatus');
this.testButton = this.container.querySelector('#testNotification');
this.triggerOptionsDiv = this.container.querySelector('.notification-trigger');
};
/**
* 設置事件監聽器
*/
NotificationSettings.prototype.setupEventListeners = function() {
const self = this;
// 開關切換事件
this.toggle.addEventListener('click', async function(e) {
const isActive = self.toggle.classList.contains('active');
if (!isActive) {
await self.enableNotifications();
} else {
self.disableNotifications();
}
});
// 測試按鈕事件
if (this.testButton) {
this.testButton.addEventListener('click', function() {
self.notificationManager.testNotification();
});
}
// 監聽頁面可見性變化,更新權限狀態
document.addEventListener('visibilitychange', function() {
self.updatePermissionStatus();
});
// 觸發模式選項事件
const triggerRadios = this.container.querySelectorAll('input[name="notificationTrigger"]');
triggerRadios.forEach(function(radio) {
radio.addEventListener('change', function() {
if (radio.checked) {
self.notificationManager.setTriggerMode(radio.value);
self.showMessage(
self.t('notification.triggerModeUpdated', '通知觸發模式已更新'),
'success'
);
}
});
});
};
/**
* 更新 UI 狀態
*/
NotificationSettings.prototype.updateUI = function() {
const settings = this.notificationManager.getSettings();
// 設定開關狀態
if (settings.enabled) {
this.toggle.classList.add('active');
} else {
this.toggle.classList.remove('active');
}
// 更新權限狀態顯示
this.updatePermissionStatus();
// 顯示/隱藏測試按鈕和觸發選項
const actionsDiv = this.container.querySelector('.notification-actions');
if (actionsDiv) {
actionsDiv.style.display = (settings.enabled && settings.permission === 'granted') ? 'block' : 'none';
}
if (this.triggerOptionsDiv) {
this.triggerOptionsDiv.style.display = (settings.enabled && settings.permission === 'granted') ? 'block' : 'none';
// 設定當前選中的觸發模式
const currentMode = settings.triggerMode || 'focusLost';
const radio = this.container.querySelector(`input[name="notificationTrigger"][value="${currentMode}"]`);
if (radio) {
radio.checked = true;
}
}
};
/**
* 啟用通知
*/
NotificationSettings.prototype.enableNotifications = async function() {
try {
const success = await this.notificationManager.enable();
if (success) {
this.showMessage(this.t('notification.enabled', '通知已啟用 ✅'), 'success');
this.updateUI();
} else {
// 權限被拒絕或其他問題
this.toggle.classList.remove('active');
this.updatePermissionStatus();
if (this.notificationManager.permission === 'denied') {
this.showMessage(
this.t('notification.permissionDenied', '瀏覽器已封鎖通知,請在瀏覽器設定中允許'),
'error'
);
} else {
this.showMessage(
this.t('notification.permissionRequired', '需要通知權限才能啟用此功能'),
'warning'
);
}
}
} catch (error) {
console.error('❌ 啟用通知失敗:', error);
this.toggle.checked = false;
this.showMessage(
this.t('notification.enableFailed', '啟用通知失敗'),
'error'
);
}
};
/**
* 停用通知
*/
NotificationSettings.prototype.disableNotifications = function() {
this.notificationManager.disable();
this.showMessage(this.t('notification.disabled', '通知已關閉'), 'info');
this.updateUI();
};
/**
* 更新權限狀態顯示
*/
NotificationSettings.prototype.updatePermissionStatus = function() {
const settings = this.notificationManager.getSettings();
if (!settings.browserSupported) {
this.statusDiv.innerHTML = `<span data-i18n="notification.notSupported"></span>`;
this.statusDiv.className = 'permission-status status-unsupported';
this.toggle.disabled = true;
return;
}
const statusMessages = {
'granted': {
icon: '✅',
text: this.t('notification.permissionGranted', '已授權'),
class: 'status-granted',
i18nKey: 'notification.permissionGranted'
},
'denied': {
icon: '❌',
text: this.t('notification.permissionDeniedStatus', '已拒絕(請在瀏覽器設定中修改)'),
class: 'status-denied',
i18nKey: 'notification.permissionDeniedStatus'
},
'default': {
icon: '⏸',
text: this.t('notification.permissionDefault', '尚未設定'),
class: 'status-default',
i18nKey: 'notification.permissionDefault'
}
};
const status = statusMessages[settings.permission] || statusMessages['default'];
// 將圖標和文字合併在同一個元素內,並加入 data-i18n 屬性以支援動態語言切換
this.statusDiv.innerHTML = `<span data-i18n="${status.i18nKey}">${status.icon} ${status.text}</span>`;
this.statusDiv.className = `permission-status ${status.class}`;
};
/**
* 顯示訊息
*/
NotificationSettings.prototype.showMessage = function(message, type) {
// 使用 Utils 的訊息顯示功能
if (Utils && Utils.showMessage) {
Utils.showMessage(message, type);
} else {
console.log(`[${type}] ${message}`);
}
};
/**
* 重新整理介面
*/
NotificationSettings.prototype.refresh = function() {
this.updateUI();
};
/**
* 清理資源
*/
NotificationSettings.prototype.destroy = function() {
// 移除事件監聽器
if (this.toggle) {
this.toggle.removeEventListener('change', this.enableNotifications);
}
if (this.testButton) {
this.testButton.removeEventListener('click', this.notificationManager.testNotification);
}
// 清空容器
if (this.container) {
this.container.innerHTML = '';
}
console.log('🧹 NotificationSettings 已清理');
};
// 匯出到全域命名空間
window.MCPFeedback.NotificationSettings = NotificationSettings;
})();

View File

@ -258,6 +258,67 @@
self.showSessionDetails();
});
}
// 复制当前会话内容按钮
const copySessionButton = DOMUtils ?
DOMUtils.safeQuerySelector('#copyCurrentSessionContent') :
document.querySelector('#copyCurrentSessionContent');
if (copySessionButton) {
copySessionButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
self.copyCurrentSessionContent();
});
}
// 复制当前用户内容按钮
const copyUserButton = DOMUtils ?
DOMUtils.safeQuerySelector('#copyCurrentUserContent') :
document.querySelector('#copyCurrentUserContent');
if (copyUserButton) {
copyUserButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
self.copyCurrentUserContent();
});
}
// 會話歷史管理按鈕 - 會話管理頁籤
// 匯出全部按鈕
const sessionTabExportAllBtn = DOMUtils ?
DOMUtils.safeQuerySelector('#sessionTabExportAllBtn') :
document.querySelector('#sessionTabExportAllBtn');
if (sessionTabExportAllBtn) {
sessionTabExportAllBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
self.exportSessionHistory();
});
}
// 清空訊息記錄按鈕
const sessionTabClearMessagesBtn = DOMUtils ?
DOMUtils.safeQuerySelector('#sessionTabClearMessagesBtn') :
document.querySelector('#sessionTabClearMessagesBtn');
if (sessionTabClearMessagesBtn) {
sessionTabClearMessagesBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
self.clearUserMessages();
});
}
// 清空所有會話按鈕
const sessionTabClearAllBtn = DOMUtils ?
DOMUtils.safeQuerySelector('#sessionTabClearAllBtn') :
document.querySelector('#sessionTabClearAllBtn');
if (sessionTabClearAllBtn) {
sessionTabClearAllBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
self.clearSessionHistory();
});
}
};
/**
@ -309,7 +370,10 @@
const currentSession = this.dataManager.getCurrentSession();
if (!currentSession) {
this.showMessage('目前沒有活躍的會話數據', 'warning');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.noActiveSession', '目前沒有活躍的會話數據') :
'目前沒有活躍的會話數據';
this.showMessage(message, 'warning');
return;
}
@ -329,7 +393,10 @@
if (sessionData) {
this.detailsModal.showSessionDetails(sessionData);
} else {
this.showMessage('找不到會話資料', 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.sessionNotFound', '找不到會話資料') :
'找不到會話資料';
this.showMessage(message, 'error');
}
};
@ -534,7 +601,10 @@
} catch (error) {
console.error('📋 匯出會話歷史失敗:', error);
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
window.MCPFeedback.Utils.showMessage('匯出失敗: ' + error.message, 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.exportFailed', { error: error.message }) :
'匯出失敗: ' + error.message;
window.MCPFeedback.Utils.showMessage(message, 'error');
}
}
};
@ -562,7 +632,10 @@
} catch (error) {
console.error('📋 匯出單一會話失敗:', error);
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
window.MCPFeedback.Utils.showMessage('匯出失敗: ' + error.message, 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.exportFailed', { error: error.message }) :
'匯出失敗: ' + error.message;
window.MCPFeedback.Utils.showMessage(message, 'error');
}
}
};
@ -598,11 +671,54 @@
} catch (error) {
console.error('📋 清空會話歷史失敗:', error);
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
window.MCPFeedback.Utils.showMessage('清空失敗: ' + error.message, 'error');
const errorMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.clearFailed', { error: error.message }) :
'清空失敗: ' + error.message;
window.MCPFeedback.Utils.showMessage(errorMessage, 'error');
}
}
};
/**
* 清空用戶訊息記錄
*/
SessionManager.prototype.clearUserMessages = function() {
if (!this.dataManager) {
console.error('📋 DataManager 未初始化');
return;
}
const i18n = window.i18nManager;
const confirmMessage = i18n ?
i18n.t('sessionHistory.userMessages.confirmClearAll') :
'確定要清空所有會話的用戶訊息記錄嗎?此操作無法復原。';
if (!confirm(confirmMessage)) {
return;
}
try {
const success = this.dataManager.clearAllUserMessages();
if (success) {
const successMessage = i18n ?
i18n.t('sessionHistory.userMessages.clearSuccess') :
'用戶訊息記錄已清空';
this.showMessage(successMessage, 'success');
} else {
const errorMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.clearFailedGeneric', '清空失敗') :
'清空失敗';
this.showMessage(errorMessage, 'error');
}
} catch (error) {
console.error('📋 清空用戶訊息記錄失敗:', error);
const errorMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.clearFailed', { error: error.message }) :
'清空失敗: ' + error.message;
this.showMessage(errorMessage, 'error');
}
};
/**
* 清理資源
*/
@ -648,6 +764,269 @@
}
};
/**
* 复制当前会话内容
*/
SessionManager.prototype.copyCurrentSessionContent = function() {
console.log('📋 复制当前会话内容...');
try {
const currentSession = this.dataManager.getCurrentSession();
if (!currentSession) {
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.noData', '沒有當前會話數據') :
'沒有當前會話數據';
this.showMessage(message, 'error');
return;
}
const content = this.formatCurrentSessionContent(currentSession);
const successMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copySuccess', '當前會話內容已複製到剪貼板') :
'當前會話內容已複製到剪貼板';
this.copyToClipboard(content, successMessage);
} catch (error) {
console.error('复制当前会话内容失败:', error);
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copyFailed', '複製失敗,請重試') :
'複製失敗,請重試';
this.showMessage(message, 'error');
}
};
/**
* 复制当前用户发送的内容
*/
SessionManager.prototype.copyCurrentUserContent = function() {
console.log('📝 复制当前用户发送的内容...');
console.log('📝 this.dataManager 存在吗?', !!this.dataManager);
try {
if (!this.dataManager) {
console.log('📝 dataManager 不存在,尝试其他方式获取数据');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.dataManagerNotInit', '數據管理器未初始化') :
'數據管理器未初始化';
this.showMessage(message, 'error');
return;
}
const currentSession = this.dataManager.getCurrentSession();
console.log('📝 当前会话数据:', currentSession);
if (!currentSession) {
console.log('📝 没有当前会话数据');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.noData', '當前會話沒有數據') :
'當前會話沒有數據';
this.showMessage(message, 'warning');
return;
}
console.log('📝 用户消息数组:', currentSession.user_messages);
console.log('📝 用户消息数组长度:', currentSession.user_messages ? currentSession.user_messages.length : 'undefined');
if (!currentSession.user_messages || currentSession.user_messages.length === 0) {
console.log('📝 没有用户消息记录');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.noUserMessages', '當前會話沒有用戶消息記錄') :
'當前會話沒有用戶消息記錄';
this.showMessage(message, 'warning');
return;
}
// 在这里也添加调试信息
console.log('📝 准备格式化用户消息,数量:', currentSession.user_messages.length);
console.log('📝 第一条消息内容:', currentSession.user_messages[0]);
const content = this.formatCurrentUserContent(currentSession.user_messages);
console.log('📝 格式化后的内容长度:', content.length);
console.log('📝 格式化后的内容预览:', content.substring(0, 200));
const successMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.userContentCopySuccess', '當前用戶內容已複製到剪貼板') :
'當前用戶內容已複製到剪貼板';
this.copyToClipboard(content, successMessage);
} catch (error) {
console.error('📝 复制当前用户内容失败:', error);
console.error('📝 错误堆栈:', error.stack);
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copyFailed', '複製失敗,請重試') :
'複製失敗,請重試';
this.showMessage(message, 'error');
}
};
/**
* 格式化当前会话内容
*/
SessionManager.prototype.formatCurrentSessionContent = function(sessionData) {
const lines = [];
lines.push('# MCP Feedback Enhanced - 当前会话内容');
lines.push('');
lines.push(`**会话ID**: ${sessionData.session_id || 'N/A'}`);
lines.push(`**项目目录**: ${sessionData.project_directory || 'N/A'}`);
lines.push(`**摘要**: ${sessionData.summary || 'N/A'}`);
lines.push(`**状态**: ${sessionData.status || 'N/A'}`);
lines.push(`**创建时间**: ${sessionData.created_at || 'N/A'}`);
lines.push(`**更新时间**: ${sessionData.updated_at || 'N/A'}`);
lines.push('');
if (sessionData.user_messages && sessionData.user_messages.length > 0) {
lines.push('## 用户消息');
sessionData.user_messages.forEach((msg, index) => {
lines.push(`### 消息 ${index + 1}`);
lines.push(msg);
lines.push('');
});
}
if (sessionData.ai_responses && sessionData.ai_responses.length > 0) {
lines.push('## AI 响应');
sessionData.ai_responses.forEach((response, index) => {
lines.push(`### 响应 ${index + 1}`);
lines.push(response);
lines.push('');
});
}
return lines.join('\n');
};
/**
* 格式化当前用户内容
*/
SessionManager.prototype.formatCurrentUserContent = function(userMessages) {
const lines = [];
lines.push('# MCP Feedback Enhanced - 用户发送内容');
lines.push('');
userMessages.forEach((msg, index) => {
lines.push(`## 消息 ${index + 1}`);
// 调试:输出完整的消息对象
console.log(`📝 消息 ${index + 1} 完整对象:`, msg);
console.log(`📝 消息 ${index + 1} 所有属性:`, Object.keys(msg));
// 添加时间戳信息 - 简化版本,直接使用当前时间
let timeStr = '未知时间';
// 检查是否有时间戳字段
if (msg.timestamp) {
// 如果时间戳看起来不正常(太小),直接使用当前时间
if (msg.timestamp < 1000000000) { // 小于2001年的时间戳可能是相对时间
timeStr = new Date().toLocaleString('zh-CN');
console.log('📝 时间戳异常,使用当前时间:', msg.timestamp);
} else {
// 正常处理时间戳
let timestamp = msg.timestamp;
if (timestamp > 1e12) {
// 毫秒时间戳
timeStr = new Date(timestamp).toLocaleString('zh-CN');
} else {
// 秒时间戳
timeStr = new Date(timestamp * 1000).toLocaleString('zh-CN');
}
}
} else {
// 没有时间戳,使用当前时间
timeStr = new Date().toLocaleString('zh-CN');
console.log('📝 没有时间戳字段,使用当前时间');
}
lines.push(`**时间**: ${timeStr}`);
// 添加提交方式
if (msg.submission_method) {
const methodText = msg.submission_method === 'auto' ? '自动提交' : '手动提交';
lines.push(`**提交方式**: ${methodText}`);
}
// 处理消息内容
if (msg.content !== undefined) {
// 完整记录模式 - 显示实际内容
lines.push(`**内容**: ${msg.content}`);
// 如果有图片,显示图片数量
if (msg.images && msg.images.length > 0) {
lines.push(`**图片数量**: ${msg.images.length}`);
}
} else if (msg.content_length !== undefined) {
// 基本统计模式 - 显示统计信息
lines.push(`**内容长度**: ${msg.content_length} 字符`);
lines.push(`**图片数量**: ${msg.image_count || 0}`);
lines.push(`**有内容**: ${msg.has_content ? '是' : '否'}`);
} else if (msg.privacy_note) {
// 隐私保护模式
lines.push(`**内容**: [内容记录已停用 - 隐私设置]`);
} else {
// 兜底情况 - 尝试显示对象的JSON格式
lines.push(`**原始数据**: ${JSON.stringify(msg, null, 2)}`);
}
lines.push('');
});
return lines.join('\n');
};
/**
* 复制到剪贴板
*/
SessionManager.prototype.copyToClipboard = function(text, successMessage) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
this.showMessage(successMessage, 'success');
}).catch(err => {
console.error('复制到剪贴板失败:', err);
this.fallbackCopyToClipboard(text, successMessage);
});
} else {
this.fallbackCopyToClipboard(text, successMessage);
}
};
/**
* 降级复制方法
*/
SessionManager.prototype.fallbackCopyToClipboard = function(text, successMessage) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
this.showMessage(successMessage, 'success');
} catch (err) {
console.error('降级复制失败:', err);
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copyFailedManual', '複製失敗,請手動複製') :
'複製失敗,請手動複製';
this.showMessage(message, 'error');
} finally {
document.body.removeChild(textArea);
}
};
/**
* 显示消息
*/
SessionManager.prototype.showMessage = function(message, type) {
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
const messageType = type === 'success' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS :
type === 'warning' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_WARNING :
window.MCPFeedback.Utils.CONSTANTS.MESSAGE_ERROR;
window.MCPFeedback.Utils.showMessage(message, messageType);
} else {
console.log(`[${type.toUpperCase()}] ${message}`);
}
};
// 全域匯出會話歷史方法
window.MCPFeedback.SessionManager.exportSessionHistory = function() {
if (window.MCPFeedback && window.MCPFeedback.app && window.MCPFeedback.app.sessionManager) {

View File

@ -62,9 +62,13 @@
if (this.currentSession && this.currentSession.session_id) {
console.log('📊 檢測到會話 ID 變更,處理舊會話:', this.currentSession.session_id, '->', sessionData.session_id);
// 將舊會話標記為完成並加入歷史記錄
// 將舊會話加入歷史記錄,保持其原有狀態
const oldSession = Object.assign({}, this.currentSession);
oldSession.status = 'completed';
// 完全保持舊會話的原有狀態,不做任何修改
// 讓服務器端負責狀態轉換,前端只負責顯示
console.log('📊 保持舊會話的原有狀態:', oldSession.status);
oldSession.completed_at = TimeUtils.getCurrentTimestamp();
// 計算持續時間
@ -309,6 +313,9 @@
// 記錄用戶最後互動時間
this.currentSession.last_user_interaction = TimeUtils.getCurrentTimestamp();
// 發送用戶消息到服務器端
this.sendUserMessageToServer(userMessage);
// 立即保存當前會話到伺服器
this.saveCurrentSessionToServer();
@ -316,6 +323,30 @@
return true;
};
/**
* 發送用戶消息到服務器端
*/
SessionDataManager.prototype.sendUserMessageToServer = function(userMessage) {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/add-user-message?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userMessage)
})
.then(function(response) {
if (response.ok) {
console.log('📊 用戶消息已發送到服務器端');
} else {
console.warn('📊 發送用戶消息到服務器端失敗:', response.status);
}
})
.catch(function(error) {
console.warn('📊 發送用戶消息到服務器端出錯:', error);
});
};
/**
* 建立用戶訊息記錄
*/
@ -453,16 +484,24 @@
};
/**
* 根據 ID 查找會話
* 根據 ID 查找會話包含完整的用戶消息數據
*/
SessionDataManager.prototype.findSessionById = function(sessionId) {
// 先檢查當前會話
if (this.currentSession && this.currentSession.session_id === sessionId) {
console.log('📊 從當前會話獲取數據:', sessionId, '用戶消息數量:', this.currentSession.user_messages ? this.currentSession.user_messages.length : 0);
return this.currentSession;
}
// 再檢查歷史記錄
return this.sessionHistory.find(s => s.session_id === sessionId) || null;
const historySession = this.sessionHistory.find(s => s.session_id === sessionId);
if (historySession) {
console.log('📊 從歷史記錄獲取數據:', sessionId, '用戶消息數量:', historySession.user_messages ? historySession.user_messages.length : 0);
return historySession;
}
console.warn('📊 找不到會話:', sessionId);
return null;
};
/**
@ -589,23 +628,26 @@
};
/**
* 從伺服器載入會話歷史
* 從伺服器載入會話歷史包含實時狀態
*/
SessionDataManager.prototype.loadFromServer = function() {
const self = this;
fetch('/api/load-session-history')
// 首先嘗試獲取實時會話狀態
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/all-sessions?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
} else {
throw new Error('伺服器回應錯誤: ' + response.status);
throw new Error('獲取實時會話狀態失敗: ' + response.status);
}
})
.then(function(data) {
if (data && Array.isArray(data.sessions)) {
// 使用實時會話狀態
self.sessionHistory = data.sessions;
console.log('📊 從伺服器載入', self.sessionHistory.length, '個會話');
console.log('📊 從伺服器載入', self.sessionHistory.length, '個實時會話狀態');
// 載入完成後進行清理和統計更新
self.cleanupExpiredSessions();
@ -621,13 +663,54 @@
self.onDataChanged();
}
} else {
console.warn('📊 伺服器回應格式錯誤:', data);
self.sessionHistory = [];
console.warn('📊 實時會話狀態回應格式錯誤,回退到歷史文件');
self.loadFromHistoryFile();
}
})
.catch(function(error) {
console.warn('📊 獲取實時會話狀態失敗,回退到歷史文件:', error);
self.loadFromHistoryFile();
});
};
// 即使沒有資料也要更新統計
/**
* 從歷史文件載入會話數據備用方案
*/
SessionDataManager.prototype.loadFromHistoryFile = function() {
const self = this;
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/load-session-history?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
} else {
throw new Error('伺服器回應錯誤: ' + response.status);
}
})
.then(function(data) {
if (data && Array.isArray(data.sessions)) {
self.sessionHistory = data.sessions;
console.log('📊 從歷史文件載入', self.sessionHistory.length, '個會話');
// 載入完成後進行清理和統計更新
self.cleanupExpiredSessions();
self.updateStats();
// 觸發歷史記錄變更回調
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}
// 觸發資料變更回調
if (self.onDataChanged) {
self.onDataChanged();
}
} else {
console.warn('📊 歷史文件回應格式錯誤:', data);
self.sessionHistory = [];
self.updateStats();
// 觸發歷史記錄變更回調(空列表)
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}
@ -638,13 +721,10 @@
}
})
.catch(function(error) {
console.warn('📊 從伺服器載入會話歷史失敗:', error);
console.warn('📊 從歷史文件載入失敗:', error);
self.sessionHistory = [];
// 載入失敗時也要更新統計
self.updateStats();
// 觸發歷史記錄變更回調(空列表)
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}
@ -698,7 +778,8 @@
lastCleanup: TimeUtils.getCurrentTimestamp()
};
fetch('/api/save-session-history', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/save-session-history?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -714,7 +795,12 @@
}
})
.then(function(result) {
console.log('📊 會話快照保存回應:', result.message);
if (result.messageCode && window.i18nManager) {
const message = window.i18nManager.t(result.messageCode, result.params);
console.log('📊 會話快照保存回應:', message);
} else {
console.log('📊 會話快照保存回應:', result.message);
}
})
.catch(function(error) {
console.error('📊 保存會話快照到伺服器失敗:', error);
@ -730,7 +816,8 @@
lastCleanup: TimeUtils.getCurrentTimestamp()
};
fetch('/api/save-session-history', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/save-session-history?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -746,7 +833,12 @@
}
})
.then(function(result) {
console.log('📊 伺服器保存回應:', result.message);
if (result.messageCode && window.i18nManager) {
const message = window.i18nManager.t(result.messageCode, result.params);
console.log('📊 伺服器保存回應:', message);
} else {
console.log('📊 伺服器保存回應:', result.message);
}
})
.catch(function(error) {
console.error('📊 保存會話歷史到伺服器失敗:', error);

View File

@ -31,7 +31,7 @@
this.currentModal = null;
this.keydownHandler = null;
console.log('🔍 SessionDetailsModal 初始化完成');
// console.log('🔍 SessionDetailsModal 初始化完成');
}
/**
@ -43,7 +43,10 @@
return;
}
console.log('🔍 顯示會話詳情:', sessionData.session_id);
// console.log('🔍 顯示會話詳情:', sessionData.session_id);
// 存储当前会话数据,供复制功能使用
this.currentSessionData = sessionData;
// 關閉現有彈窗
this.closeModal();
@ -59,7 +62,7 @@
* 格式化會話詳情
*/
SessionDetailsModal.prototype.formatSessionDetails = function(sessionData) {
console.log('🔍 格式化會話詳情:', sessionData);
// console.log('🔍 格式化會話詳情:', sessionData);
// 處理會話 ID - 顯示完整 session ID
const sessionId = sessionData.session_id || '未知';
@ -166,7 +169,12 @@
</div>
<div class="detail-row">
<span class="detail-label">${i18n ? i18n.t('sessionManagement.aiSummary') : 'AI 摘要'}:</span>
<div class="detail-value summary">${this.escapeHtml(details.summary)}</div>
<div class="detail-value summary">
<div class="summary-actions">
<button class="btn-copy-summary" title="複製摘要" aria-label="複製摘要">📋</button>
</div>
<div class="summary-content">${this.renderMarkdownSafely(details.summary)}</div>
</div>
</div>
${this.createUserMessagesSection(details)}
</div>
@ -241,11 +249,12 @@
}
messagesHtml += `
<div class="user-message-item">
<div class="user-message-item" data-message-index="${index}">
<div class="message-header">
<span class="message-index">#${index + 1}</span>
<span class="message-time">${timestamp}</span>
<span class="message-method">${submissionMethod}</span>
<button class="btn-copy-message" title="複製消息內容" aria-label="複製消息內容" data-message-content="${this.escapeHtml(message.content)}">📋</button>
</div>
${contentHtml}
</div>
@ -310,6 +319,24 @@
};
document.addEventListener('keydown', this.keydownHandler);
}
// 复制摘要按钮
const copyBtn = this.currentModal.querySelector('.btn-copy-summary');
if (copyBtn) {
DOMUtils.addEventListener(copyBtn, 'click', function() {
self.copySummaryToClipboard();
});
}
// 复制用户消息按钮
const copyMessageBtns = this.currentModal.querySelectorAll('.btn-copy-message');
copyMessageBtns.forEach(function(btn) {
DOMUtils.addEventListener(btn, 'click', function(e) {
e.stopPropagation(); // 防止事件冒泡
const messageContent = btn.getAttribute('data-message-content');
self.copyMessageToClipboard(messageContent);
});
});
};
/**
@ -319,7 +346,7 @@
if (!this.currentModal) return;
// 彈窗已經通過 CSS 動畫自動顯示,無需額外處理
console.log('🔍 會話詳情彈窗已顯示');
// console.log('🔍 會話詳情彈窗已顯示');
};
/**
@ -355,12 +382,173 @@
*/
SessionDetailsModal.prototype.escapeHtml = function(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
/**
* 安全地渲染 Markdown 內容
*/
SessionDetailsModal.prototype.renderMarkdownSafely = function(content) {
if (!content) return '';
try {
// 檢查 marked 和 DOMPurify 是否可用
if (typeof window.marked === 'undefined' || typeof window.DOMPurify === 'undefined') {
console.warn('⚠️ Markdown 庫未載入,使用純文字顯示');
return this.escapeHtml(content);
}
// 使用 marked 解析 Markdown
const htmlContent = window.marked.parse(content);
// 使用 DOMPurify 清理 HTML
const cleanHtml = window.DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'a', 'hr', 'del', 's', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
ALLOWED_ATTR: ['href', 'title', 'class', 'align', 'style'],
ALLOW_DATA_ATTR: false
});
return cleanHtml;
} catch (error) {
console.error('❌ Markdown 渲染失敗:', error);
return this.escapeHtml(content);
}
};
/**
* 傳統複製文字到剪貼板的方法
*/
SessionDetailsModal.prototype.fallbackCopyTextToClipboard = function(text, successMessage) {
const self = this;
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
// console.log('✅ 內容已複製到剪貼板(傳統方法)');
self.showToast(successMessage, 'success');
} else {
console.error('❌ 複製失敗(傳統方法)');
self.showToast('❌ 複製失敗,請手動複製', 'error');
}
} catch (err) {
console.error('❌ 複製失敗:', err);
self.showToast('❌ 複製失敗,請手動複製', 'error');
} finally {
document.body.removeChild(textArea);
}
};
/**
* 複製摘要內容到剪貼板
*/
SessionDetailsModal.prototype.copySummaryToClipboard = function() {
const self = this;
try {
// 獲取原始摘要內容Markdown 原始碼)
const summaryContent = this.currentSessionData && this.currentSessionData.summary ?
this.currentSessionData.summary : '';
if (!summaryContent) {
console.warn('⚠️ 沒有摘要內容可複製');
return;
}
// 使用現代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(summaryContent).then(function() {
// console.log('✅ 摘要內容已複製到剪貼板');
self.showToast('✅ 摘要已複製到剪貼板', 'success');
}).catch(function(err) {
console.error('❌ 複製失敗:', err);
// 降級到傳統方法
self.fallbackCopyTextToClipboard(summaryContent, '✅ 摘要已複製到剪貼板');
});
} else {
// 降級到傳統方法
this.fallbackCopyTextToClipboard(summaryContent, '✅ 摘要已複製到剪貼板');
}
} catch (error) {
console.error('❌ 複製摘要時發生錯誤:', error);
this.showToast('❌ 複製失敗,請手動複製', 'error');
}
};
/**
* 複製用戶消息內容到剪貼板
*/
SessionDetailsModal.prototype.copyMessageToClipboard = function(messageContent) {
if (!messageContent) {
console.warn('⚠️ 沒有消息內容可複製');
return;
}
const self = this;
try {
// 使用現代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(messageContent).then(function() {
// console.log('✅ 用戶消息已複製到剪貼板');
self.showToast('✅ 消息已複製到剪貼板', 'success');
}).catch(function(err) {
console.error('❌ 複製失敗:', err);
// 降級到傳統方法
self.fallbackCopyTextToClipboard(messageContent, '✅ 消息已複製到剪貼板');
});
} else {
// 降級到傳統方法
this.fallbackCopyTextToClipboard(messageContent, '✅ 消息已複製到剪貼板');
}
} catch (error) {
console.error('❌ 複製用戶消息時發生錯誤:', error);
this.showToast('❌ 複製失敗,請手動複製', 'error');
}
};
/**
* 顯示提示消息
*/
SessionDetailsModal.prototype.showToast = function(message, type) {
// 創建提示元素
const toast = document.createElement('div');
toast.className = 'copy-toast copy-toast-' + type;
toast.textContent = message;
// 添加到彈窗中
if (this.currentModal) {
this.currentModal.appendChild(toast);
// 顯示動畫
setTimeout(function() {
toast.classList.add('show');
}, 10);
// 自動隱藏
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 2000);
}
};
/**
* 檢查是否有彈窗開啟
*/
@ -395,12 +583,12 @@
*/
SessionDetailsModal.prototype.cleanup = function() {
this.forceCloseAll();
console.log('🔍 SessionDetailsModal 清理完成');
// console.log('🔍 SessionDetailsModal 清理完成');
};
// 將 SessionDetailsModal 加入命名空間
window.MCPFeedback.Session.DetailsModal = SessionDetailsModal;
console.log('✅ SessionDetailsModal 模組載入完成');
// console.log('✅ SessionDetailsModal 模組載入完成');
})();

View File

@ -20,6 +20,9 @@
new window.MCPFeedback.Logger({ moduleName: 'SessionUIRenderer' }) :
console;
const StatusUtils = window.MCPFeedback.Utils.Status;
// 調試模式標誌 - 生產環境應設為 false
const DEBUG_MODE = false;
/**
* 會話 UI 渲染器
@ -80,31 +83,31 @@
* 初始化專案路徑顯示
*/
SessionUIRenderer.prototype.initializeProjectPathDisplay = function() {
console.log('🎨 初始化專案路徑顯示');
if (DEBUG_MODE) console.log('🎨 初始化專案路徑顯示');
const projectPathElement = document.getElementById('projectPathDisplay');
console.log('🎨 初始化時找到專案路徑元素:', !!projectPathElement);
if (DEBUG_MODE) console.log('🎨 初始化時找到專案路徑元素:', !!projectPathElement);
if (projectPathElement) {
const fullPath = projectPathElement.getAttribute('data-full-path');
console.log('🎨 初始化時的完整路徑:', fullPath);
if (DEBUG_MODE) console.log('🎨 初始化時的完整路徑:', fullPath);
if (fullPath) {
// 使用工具函數截斷路徑
const pathResult = window.MCPFeedback.Utils.truncatePathFromRight(fullPath, 2, 40);
console.log('🎨 初始化時路徑處理:', { fullPath, shortPath: pathResult.truncated });
if (DEBUG_MODE) console.log('🎨 初始化時路徑處理:', { fullPath, shortPath: pathResult.truncated });
// 更新顯示文字
DOMUtils.safeSetTextContent(projectPathElement, pathResult.truncated);
// 添加點擊複製功能
if (!projectPathElement.hasAttribute('data-copy-handler')) {
console.log('🎨 初始化時添加點擊複製功能');
if (DEBUG_MODE) console.log('🎨 初始化時添加點擊複製功能');
projectPathElement.setAttribute('data-copy-handler', 'true');
projectPathElement.addEventListener('click', function() {
console.log('🎨 初始化的專案路徑被點擊');
if (DEBUG_MODE) console.log('🎨 初始化的專案路徑被點擊');
const fullPath = this.getAttribute('data-full-path');
console.log('🎨 初始化時準備複製路徑:', fullPath);
if (DEBUG_MODE) console.log('🎨 初始化時準備複製路徑:', fullPath);
if (fullPath) {
const successMessage = window.i18nManager ?
@ -114,12 +117,12 @@
window.i18nManager.t('app.pathCopyFailed', '複製路徑失敗') :
'複製路徑失敗';
console.log('🎨 初始化時調用複製函數');
if (DEBUG_MODE) console.log('🎨 初始化時調用複製函數');
window.MCPFeedback.Utils.copyToClipboard(fullPath, successMessage, errorMessage);
}
});
} else {
console.log('🎨 初始化時點擊複製功能已存在');
if (DEBUG_MODE) console.log('🎨 初始化時點擊複製功能已存在');
}
// 添加 tooltip 位置自動調整
@ -168,7 +171,7 @@
* 執行實際的當前會話渲染
*/
SessionUIRenderer.prototype._performCurrentSessionRender = function(sessionData, isNewSession) {
console.log('🎨 渲染當前會話:', sessionData);
if (DEBUG_MODE) console.log('🎨 渲染當前會話:', sessionData);
// 更新快取
this.lastRenderedData.currentSessionId = sessionData.session_id;
@ -176,7 +179,7 @@
// 如果是新會話,重置活躍時間定時器
if (isNewSession) {
console.log('🎨 檢測到新會話,重置活躍時間定時器');
if (DEBUG_MODE) console.log('🎨 檢測到新會話,重置活躍時間定時器');
this.resetActiveTimeTimer();
}
@ -258,17 +261,17 @@
* 更新頂部狀態列的專案路徑顯示
*/
SessionUIRenderer.prototype.updateTopProjectPathDisplay = function(sessionData) {
console.log('🎨 updateProjectPathDisplay 被調用:', sessionData);
if (DEBUG_MODE) console.log('🎨 updateProjectPathDisplay 被調用:', sessionData);
const projectPathElement = document.getElementById('projectPathDisplay');
console.log('🎨 找到專案路徑元素:', !!projectPathElement);
if (DEBUG_MODE) console.log('🎨 找到專案路徑元素:', !!projectPathElement);
if (projectPathElement && sessionData.project_directory) {
const fullPath = sessionData.project_directory;
// 使用工具函數截斷路徑
const pathResult = window.MCPFeedback.Utils.truncatePathFromRight(fullPath, 2, 40);
console.log('🎨 路徑處理:', { fullPath, shortPath: pathResult.truncated });
if (DEBUG_MODE) console.log('🎨 路徑處理:', { fullPath, shortPath: pathResult.truncated });
// 更新顯示文字
DOMUtils.safeSetTextContent(projectPathElement, pathResult.truncated);
@ -278,12 +281,12 @@
// 添加點擊複製功能(如果還沒有)
if (!projectPathElement.hasAttribute('data-copy-handler')) {
console.log('🎨 添加點擊複製功能');
if (DEBUG_MODE) console.log('🎨 添加點擊複製功能');
projectPathElement.setAttribute('data-copy-handler', 'true');
projectPathElement.addEventListener('click', function() {
console.log('🎨 專案路徑被點擊');
if (DEBUG_MODE) console.log('🎨 專案路徑被點擊');
const fullPath = this.getAttribute('data-full-path');
console.log('🎨 準備複製路徑:', fullPath);
if (DEBUG_MODE) console.log('🎨 準備複製路徑:', fullPath);
if (fullPath) {
const successMessage = window.i18nManager ?
@ -293,12 +296,12 @@
window.i18nManager.t('app.pathCopyFailed', '複製路徑失敗') :
'複製路徑失敗';
console.log('🎨 調用複製函數');
if (DEBUG_MODE) console.log('🎨 調用複製函數');
window.MCPFeedback.Utils.copyToClipboard(fullPath, successMessage, errorMessage);
}
});
} else {
console.log('🎨 點擊複製功能已存在');
if (DEBUG_MODE) console.log('🎨 點擊複製功能已存在');
}
// 添加 tooltip 位置自動調整
@ -352,7 +355,7 @@
SessionUIRenderer.prototype.updateSessionStatusBar = function(sessionData) {
if (!sessionData) return;
console.log('🎨 更新會話狀態列:', sessionData);
if (DEBUG_MODE) console.log('🎨 更新會話狀態列:', sessionData);
// 更新當前會話 ID - 顯示縮短版本完整ID存在data-full-id中
const currentSessionElement = document.getElementById('currentSessionId');
@ -413,7 +416,7 @@
* 執行實際的會話歷史渲染
*/
SessionUIRenderer.prototype._performHistoryRender = function(sessionHistory) {
console.log('🎨 渲染會話歷史:', sessionHistory.length, '個會話');
if (DEBUG_MODE) console.log('🎨 渲染會話歷史:', sessionHistory.length, '個會話');
// 更新快取
this.lastRenderedData.historyLength = sessionHistory.length;
@ -477,22 +480,47 @@
SessionUIRenderer.prototype.createSessionHeader = function(sessionData) {
const header = DOMUtils.createElement('div', { className: 'session-header' });
// 會話 ID
const sessionIdLabel = window.i18nManager ? window.i18nManager.t('sessionManagement.sessionId') : '會話 ID';
const sessionId = DOMUtils.createElement('div', {
className: 'session-id',
textContent: sessionIdLabel + ': ' + (sessionData.session_id || '').substring(0, 8) + '...'
// 會話 ID 容器
const sessionIdContainer = DOMUtils.createElement('div', {
className: 'session-id'
});
// 會話 ID 標籤
const sessionIdLabel = DOMUtils.createElement('span', {
attributes: {
'data-i18n': 'sessionManagement.sessionId'
},
textContent: window.i18nManager ? window.i18nManager.t('sessionManagement.sessionId') : '會話 ID'
});
// 會話 ID 值
const sessionIdValue = DOMUtils.createElement('span', {
textContent: ': ' + (sessionData.session_id || '').substring(0, 8) + '...'
});
sessionIdContainer.appendChild(sessionIdLabel);
sessionIdContainer.appendChild(sessionIdValue);
// 狀態徽章
const statusContainer = DOMUtils.createElement('div', { className: 'session-status' });
const statusText = StatusUtils.getStatusText(sessionData.status);
// 添加調試信息
if (DEBUG_MODE) {
console.log('🎨 會話狀態調試:', {
sessionId: sessionData.session_id ? sessionData.session_id.substring(0, 8) + '...' : 'unknown',
rawStatus: sessionData.status,
displayText: statusText
});
}
const statusBadge = DOMUtils.createElement('span', {
className: 'status-badge ' + (sessionData.status || 'waiting'),
textContent: StatusUtils.getStatusText(sessionData.status)
textContent: statusText
});
statusContainer.appendChild(statusBadge);
header.appendChild(sessionId);
header.appendChild(sessionIdContainer);
header.appendChild(statusContainer);
return header;
@ -504,31 +532,57 @@
SessionUIRenderer.prototype.createSessionInfo = function(sessionData, isHistory) {
const info = DOMUtils.createElement('div', { className: 'session-info' });
// 時間資訊
// 時間資訊容器
const timeContainer = DOMUtils.createElement('div', {
className: 'session-time'
});
// 時間標籤
const timeLabelKey = isHistory ? 'sessionManagement.createdTime' : 'sessionManagement.createdTime';
const timeLabel = DOMUtils.createElement('span', {
attributes: {
'data-i18n': timeLabelKey
},
textContent: window.i18nManager ? window.i18nManager.t(timeLabelKey) : '建立時間'
});
// 時間值
const timeText = sessionData.created_at ?
TimeUtils.formatTimestamp(sessionData.created_at, { format: 'time' }) :
'--:--:--';
const timeLabel = isHistory ?
(window.i18nManager ? window.i18nManager.t('sessionManagement.sessionDetails.duration') : '完成時間') :
(window.i18nManager ? window.i18nManager.t('sessionManagement.createdTime') : '建立時間');
const timeElement = DOMUtils.createElement('div', {
className: 'session-time',
textContent: timeLabel + ': ' + timeText
const timeValue = DOMUtils.createElement('span', {
textContent: ': ' + timeText
});
info.appendChild(timeElement);
timeContainer.appendChild(timeLabel);
timeContainer.appendChild(timeValue);
info.appendChild(timeContainer);
// 歷史會話顯示持續時間
if (isHistory) {
const duration = this.calculateDisplayDuration(sessionData);
const durationLabel = window.i18nManager ? window.i18nManager.t('sessionManagement.sessionDetails.duration') : '持續時間';
const durationElement = DOMUtils.createElement('div', {
className: 'session-duration',
textContent: durationLabel + ': ' + duration
// 持續時間容器
const durationContainer = DOMUtils.createElement('div', {
className: 'session-duration'
});
info.appendChild(durationElement);
// 持續時間標籤
const durationLabel = DOMUtils.createElement('span', {
attributes: {
'data-i18n': 'sessionManagement.sessionDetails.duration'
},
textContent: window.i18nManager ? window.i18nManager.t('sessionManagement.sessionDetails.duration') : '持續時間'
});
// 持續時間值
const durationValue = DOMUtils.createElement('span', {
textContent: ': ' + duration
});
durationContainer.appendChild(durationLabel);
durationContainer.appendChild(durationValue);
info.appendChild(durationContainer);
}
return info;
@ -555,13 +609,13 @@
SessionUIRenderer.prototype.createSessionActions = function(sessionData, isHistory) {
const actions = DOMUtils.createElement('div', { className: 'session-actions' });
const buttonText = isHistory ?
(window.i18nManager ? window.i18nManager.t('sessionManagement.viewDetails') : '查看') :
(window.i18nManager ? window.i18nManager.t('sessionManagement.viewDetails') : '詳細資訊');
// 查看詳情按鈕
const viewButton = DOMUtils.createElement('button', {
className: 'btn-small',
textContent: buttonText
attributes: {
'data-i18n': 'sessionManagement.viewDetails'
},
textContent: window.i18nManager ? window.i18nManager.t('sessionManagement.viewDetails') : '詳細資訊'
});
// 添加查看詳情點擊事件
@ -577,7 +631,10 @@
if (isHistory) {
const exportButton = DOMUtils.createElement('button', {
className: 'btn-small btn-export',
textContent: window.i18nManager ? window.i18nManager.t('sessionHistory.management.exportSingle') : '匯出',
attributes: {
'data-i18n': 'sessionHistory.management.exportSingle'
},
textContent: window.i18nManager ? window.i18nManager.t('sessionHistory.management.exportSingle') : '匯出此會話',
style: 'margin-left: 4px; font-size: 11px; padding: 2px 6px;'
});
@ -686,7 +743,7 @@
self.updateActiveTime();
}, 1000);
console.log('🎨 活躍時間定時器已啟動');
if (DEBUG_MODE) console.log('🎨 活躍時間定時器已啟動');
};
/**
@ -696,7 +753,7 @@
if (this.activeTimeTimer) {
clearInterval(this.activeTimeTimer);
this.activeTimeTimer = null;
console.log('🎨 活躍時間定時器已停止');
if (DEBUG_MODE) console.log('🎨 活躍時間定時器已停止');
}
};
@ -749,12 +806,12 @@
currentSessionId: null
};
console.log('🎨 SessionUIRenderer 清理完成');
if (DEBUG_MODE) console.log('🎨 SessionUIRenderer 清理完成');
};
// 將 SessionUIRenderer 加入命名空間
window.MCPFeedback.Session.UIRenderer = SessionUIRenderer;
console.log('✅ SessionUIRenderer 模組載入完成');
if (DEBUG_MODE) console.log('✅ SessionUIRenderer 模組載入完成');
})();

View File

@ -23,14 +23,17 @@
function SettingsManager(options) {
options = options || {};
// 從 i18nManager 獲取當前語言作為預設值
const defaultLanguage = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
// 預設設定
this.defaultSettings = {
layoutMode: 'combined-vertical',
autoClose: false,
language: 'zh-TW',
language: defaultLanguage, // 使用 i18nManager 的當前語言
imageSizeLimit: 0,
enableBase64Detail: false,
activeTab: 'combined',
// 移除 activeTab - 頁籤切換無需持久化
sessionPanelCollapsed: false,
// 自動定時提交設定
autoSubmitEnabled: false,
@ -47,7 +50,14 @@
userMessageRecordingEnabled: true,
userMessagePrivacyLevel: 'full', // 'full', 'basic', 'disabled'
// UI 元素尺寸設定
combinedFeedbackTextHeight: 150 // combinedFeedbackText textarea 的高度px
combinedFeedbackTextHeight: 150, // combinedFeedbackText textarea 的高度px
// 會話超時設定
sessionTimeoutEnabled: false, // 預設關閉
sessionTimeoutSeconds: 3600, // 預設 1 小時(秒)
// 自動執行命令設定
autoCommandEnabled: true, // 是否啟用自動執行命令
commandOnNewSession: '', // 新會話建立時執行的命令
commandOnFeedbackSubmit: '' // 提交回饋後執行的命令
};
// 當前設定
@ -58,12 +68,7 @@
this.onLanguageChange = options.onLanguageChange || null;
this.onAutoSubmitStateChange = options.onAutoSubmitStateChange || null;
// 防抖機制相關
this.saveToServerDebounceTimer = null;
this.saveToServerDebounceDelay = options.saveDebounceDelay || 500; // 預設 500ms 防抖延遲
this.pendingServerSave = false;
console.log('✅ SettingsManager 建構函數初始化完成,防抖延遲:', this.saveToServerDebounceDelay + 'ms');
console.log('✅ SettingsManager 建構函數初始化完成 - 即時保存模式');
}
/**
@ -74,29 +79,28 @@
return new Promise(function(resolve, reject) {
logger.info('開始載入設定...');
// 優先從伺服器端載入設定
// 從伺服器端載入設定
self.loadFromServer()
.then(function(serverSettings) {
if (serverSettings && Object.keys(serverSettings).length > 0) {
self.currentSettings = self.mergeSettings(self.defaultSettings, serverSettings);
logger.info('從伺服器端載入設定成功:', self.currentSettings);
// 同步到 localStorage
self.saveToLocalStorage();
resolve(self.currentSettings);
} else {
// 回退到 localStorage
return self.loadFromLocalStorage();
}
})
.then(function(localSettings) {
if (localSettings) {
self.currentSettings = self.mergeSettings(self.defaultSettings, localSettings);
console.log('從 localStorage 載入設定:', self.currentSettings);
} else {
console.log('沒有找到設定,使用預設值');
self.currentSettings = Utils.deepClone(self.defaultSettings);
}
// 同步語言設定到 i18nManager
if (self.currentSettings.language && window.i18nManager) {
const currentI18nLanguage = window.i18nManager.getCurrentLanguage();
if (self.currentSettings.language !== currentI18nLanguage) {
console.log('🔧 SettingsManager.loadSettings: 同步語言設定到 i18nManager');
console.log(' 從:', currentI18nLanguage, '到:', self.currentSettings.language);
window.i18nManager.setLanguage(self.currentSettings.language);
}
}
resolve(self.currentSettings);
})
.catch(function(error) {
@ -111,7 +115,8 @@
* 從伺服器載入設定
*/
SettingsManager.prototype.loadFromServer = function() {
return fetch('/api/load-settings')
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
return fetch('/api/load-settings?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
@ -125,27 +130,7 @@
});
};
/**
* localStorage 載入設定
*/
SettingsManager.prototype.loadFromLocalStorage = function() {
if (!Utils.isLocalStorageSupported()) {
return Promise.resolve(null);
}
try {
const localSettings = localStorage.getItem('mcp-feedback-settings');
if (localSettings) {
const parsed = Utils.safeJsonParse(localSettings, null);
console.log('從 localStorage 載入設定:', parsed);
return Promise.resolve(parsed);
}
} catch (error) {
console.warn('從 localStorage 載入設定失敗:', error);
}
return Promise.resolve(null);
};
/**
* 保存設定
@ -157,10 +142,7 @@
logger.debug('保存設定:', this.currentSettings);
// 保存到 localStorage
this.saveToLocalStorage();
// 同步保存到伺服器端
// 只保存到伺服器端
this.saveToServer();
// 觸發回調
@ -171,39 +153,13 @@
return this.currentSettings;
};
/**
* 保存到 localStorage
*/
SettingsManager.prototype.saveToLocalStorage = function() {
if (!Utils.isLocalStorageSupported()) {
return;
}
try {
localStorage.setItem('mcp-feedback-settings', JSON.stringify(this.currentSettings));
} catch (error) {
console.error('保存設定到 localStorage 失敗:', error);
}
};
/**
* 保存到伺服器帶防抖機制
* 保存到伺服器即時保存
*/
SettingsManager.prototype.saveToServer = function() {
const self = this;
// 清除之前的定時器
if (self.saveToServerDebounceTimer) {
clearTimeout(self.saveToServerDebounceTimer);
}
// 標記有待處理的保存操作
self.pendingServerSave = true;
// 設置新的防抖定時器
self.saveToServerDebounceTimer = setTimeout(function() {
self._performServerSave();
}, self.saveToServerDebounceDelay);
this._performServerSave();
};
/**
@ -212,13 +168,8 @@
SettingsManager.prototype._performServerSave = function() {
const self = this;
if (!self.pendingServerSave) {
return;
}
self.pendingServerSave = false;
fetch('/api/save-settings', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/save-settings?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -226,10 +177,18 @@
body: JSON.stringify(self.currentSettings)
})
.then(function(response) {
if (response.ok) {
console.log('設定已同步到伺服器端');
return response.json();
})
.then(function(data) {
if (data.status === 'success') {
console.log('設定已即時同步到伺服器端');
// 處理訊息代碼
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode, data.params);
console.log('伺服器回應:', message);
}
} else {
console.warn('同步設定到伺服器端失敗:', response.status);
console.warn('同步設定到伺服器端失敗:', data);
}
})
.catch(function(error) {
@ -237,20 +196,7 @@
});
};
/**
* 立即保存到伺服器跳過防抖機制
* 用於重要操作如語言變更重置設定等
*/
SettingsManager.prototype.saveToServerImmediate = function() {
// 清除防抖定時器
if (this.saveToServerDebounceTimer) {
clearTimeout(this.saveToServerDebounceTimer);
this.saveToServerDebounceTimer = null;
}
// 立即執行保存
this._performServerSave();
};
/**
* 合併設定
@ -283,23 +229,15 @@
SettingsManager.prototype.set = function(key, value) {
const oldValue = this.currentSettings[key];
this.currentSettings[key] = value;
// 特殊處理語言變更
if (key === 'language' && oldValue !== value) {
this.handleLanguageChange(value);
// 語言變更是重要操作,立即保存
this.saveToLocalStorage();
this.saveToServerImmediate();
// 觸發回調
if (this.onSettingsChange) {
this.onSettingsChange(this.currentSettings);
}
} else {
// 一般設定變更使用防抖保存
this.saveSettings();
}
// 所有設定變更都即時保存
this.saveSettings();
return this;
};
@ -332,15 +270,11 @@
* 處理語言變更
*/
SettingsManager.prototype.handleLanguageChange = function(newLanguage) {
console.log('語言設定變更: ' + newLanguage);
console.log('🔄 SettingsManager.handleLanguageChange: ' + newLanguage);
// 同步到 localStorage
if (Utils.isLocalStorageSupported()) {
localStorage.setItem('language', newLanguage);
}
// 通知國際化系統
// 通知國際化系統(統一由 SettingsManager 管理)
if (window.i18nManager) {
// 使用 setLanguage 方法確保正確更新
window.i18nManager.setLanguage(newLanguage);
}
@ -361,17 +295,11 @@
SettingsManager.prototype.resetSettings = function() {
console.log('重置所有設定');
// 清除 localStorage
if (Utils.isLocalStorageSupported()) {
localStorage.removeItem('mcp-feedback-settings');
}
// 重置為預設值
this.currentSettings = Utils.deepClone(this.defaultSettings);
// 立即保存重置後的設定(重要操作)
this.saveToLocalStorage();
this.saveToServerImmediate();
// 立即保存重置後的設定到伺服器
this.saveToServer();
// 觸發回調
if (this.onSettingsChange) {
@ -496,6 +424,9 @@
// 應用用戶訊息記錄設定
this.applyUserMessageSettings();
// 應用會話超時設定
this.applySessionTimeoutSettings();
};
/**
@ -678,6 +609,28 @@
this.updatePrivacyLevelDescription(this.currentSettings.userMessagePrivacyLevel);
};
/**
* 應用會話超時設定
*/
SettingsManager.prototype.applySessionTimeoutSettings = function() {
// 更新會話超時啟用開關
const sessionTimeoutEnabled = Utils.safeQuerySelector('#sessionTimeoutEnabled');
if (sessionTimeoutEnabled) {
sessionTimeoutEnabled.checked = this.currentSettings.sessionTimeoutEnabled;
}
// 更新會話超時時間輸入框
const sessionTimeoutSeconds = Utils.safeQuerySelector('#sessionTimeoutSeconds');
if (sessionTimeoutSeconds) {
sessionTimeoutSeconds.value = this.currentSettings.sessionTimeoutSeconds;
}
console.log('會話超時設定已應用到 UI:', {
enabled: this.currentSettings.sessionTimeoutEnabled,
seconds: this.currentSettings.sessionTimeoutSeconds
});
};
/**
* 更新隱私等級描述文字
*/
@ -788,7 +741,10 @@
try {
// 如果要啟用自動提交,檢查是否已選擇提示詞
if (newValue && (!currentPromptId || currentPromptId === '')) {
Utils.showMessage('請先選擇一個提示詞作為自動提交內容', Utils.CONSTANTS.MESSAGE_WARNING);
const message = window.i18nManager ?
window.i18nManager.t('settingsUI.autoCommitNoPrompt', '請先選擇一個提示詞作為自動提交內容') :
'請先選擇一個提示詞作為自動提交內容';
Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_WARNING);
return;
}
@ -972,6 +928,59 @@
});
}
// 會話超時啟用開關
const sessionTimeoutEnabled = Utils.safeQuerySelector('#sessionTimeoutEnabled');
if (sessionTimeoutEnabled) {
sessionTimeoutEnabled.addEventListener('change', function() {
const newValue = sessionTimeoutEnabled.checked;
self.set('sessionTimeoutEnabled', newValue);
console.log('會話超時狀態已更新:', newValue);
// 觸發 WebSocket 通知後端更新超時設定
if (window.MCPFeedback && window.MCPFeedback.app && window.MCPFeedback.app.webSocketManager) {
window.MCPFeedback.app.webSocketManager.send({
type: 'update_timeout_settings',
settings: {
enabled: newValue,
seconds: self.get('sessionTimeoutSeconds')
}
});
}
});
}
// 會話超時時間設定
const sessionTimeoutSeconds = Utils.safeQuerySelector('#sessionTimeoutSeconds');
if (sessionTimeoutSeconds) {
sessionTimeoutSeconds.addEventListener('change', function(e) {
const seconds = parseInt(e.target.value);
// 驗證輸入值範圍
if (isNaN(seconds) || seconds < 300) {
e.target.value = 300;
self.set('sessionTimeoutSeconds', 300);
} else if (seconds > 86400) {
e.target.value = 86400;
self.set('sessionTimeoutSeconds', 86400);
} else {
self.set('sessionTimeoutSeconds', seconds);
}
console.log('會話超時時間已更新:', self.get('sessionTimeoutSeconds'), '秒');
// 觸發 WebSocket 通知後端更新超時設定
if (window.MCPFeedback && window.MCPFeedback.app && window.MCPFeedback.app.webSocketManager) {
window.MCPFeedback.app.webSocketManager.send({
type: 'update_timeout_settings',
settings: {
enabled: self.get('sessionTimeoutEnabled'),
seconds: self.get('sessionTimeoutSeconds')
}
});
}
});
}
// 重置設定
const resetBtn = Utils.safeQuerySelector('#resetSettingsBtn');
if (resetBtn) {

View File

@ -232,4 +232,4 @@
console.log('✅ TabManager 模組載入完成');
})();
})();

View File

@ -74,7 +74,6 @@
this.tabContents = document.querySelectorAll('.tab-content');
// 回饋相關元素
this.feedbackText = Utils.safeQuerySelector('#feedbackText');
this.submitBtn = Utils.safeQuerySelector('#submitBtn');
console.log('✅ UI 元素初始化完成');
@ -257,15 +256,12 @@
* 更新回饋輸入框狀態
*/
UIManager.prototype.updateFeedbackInputs = function() {
const feedbackInputs = [
Utils.safeQuerySelector('#feedbackText'),
Utils.safeQuerySelector('#combinedFeedbackText')
].filter(function(input) { return input !== null; });
const feedbackInput = Utils.safeQuerySelector('#combinedFeedbackText');
const canInput = this.feedbackState === Utils.CONSTANTS.FEEDBACK_WAITING;
feedbackInputs.forEach(function(input) {
input.disabled = !canInput;
});
if (feedbackInput) {
feedbackInput.disabled = !canInput;
}
};
/**
@ -486,20 +482,22 @@
/**
* 重置回饋表單
* @param {boolean} clearText - 是否清空文字內容預設為 false
*/
UIManager.prototype.resetFeedbackForm = function() {
UIManager.prototype.resetFeedbackForm = function(clearText) {
console.log('🔄 重置回饋表單...');
// 清空回饋輸入
const feedbackInputs = [
Utils.safeQuerySelector('#feedbackText'),
Utils.safeQuerySelector('#combinedFeedbackText')
].filter(function(input) { return input !== null; });
feedbackInputs.forEach(function(input) {
input.value = '';
input.disabled = false;
});
// 根據參數決定是否清空回饋輸入
const feedbackInput = Utils.safeQuerySelector('#combinedFeedbackText');
if (feedbackInput) {
if (clearText === true) {
feedbackInput.value = '';
console.log('📝 已清空文字內容');
}
// 只有在等待狀態才啟用輸入框
const canInput = this.feedbackState === Utils.CONSTANTS.FEEDBACK_WAITING;
feedbackInput.disabled = !canInput;
}
// 重新啟用提交按鈕
const submitButtons = [

View File

@ -169,8 +169,12 @@
* @returns {Promise<boolean>} 複製是否成功
*/
copyToClipboard: function(text, successMessage, errorMessage) {
successMessage = successMessage || '已複製到剪貼板';
errorMessage = errorMessage || '複製失敗';
successMessage = successMessage || (window.i18nManager ?
window.i18nManager.t('utils.copySuccess', '已複製到剪貼板') :
'已複製到剪貼板');
errorMessage = errorMessage || (window.i18nManager ?
window.i18nManager.t('utils.copyError', '複製失敗') :
'複製失敗');
return new Promise(function(resolve) {
if (navigator.clipboard && navigator.clipboard.writeText) {
@ -262,7 +266,115 @@
* @param {string} type - 訊息類型 (success, error, warning, info)
* @param {number} duration - 顯示時間毫秒
*/
showMessage: function(message, type, duration) {
showMessage: function(messageOrCode, type, duration) {
// 處理訊息代碼物件
let actualMessage = messageOrCode;
let actualType = type || 'info';
if (typeof messageOrCode === 'object' && messageOrCode.code) {
// 使用 i18n 系統翻譯訊息代碼
if (window.i18nManager) {
actualMessage = window.i18nManager.t(messageOrCode.code, messageOrCode.params);
} else {
// 改善 fallback 機制:提供基本的英文訊息
actualMessage = this.getFallbackMessage(messageOrCode.code, messageOrCode.params);
}
// 使用訊息物件中的嚴重程度
actualType = messageOrCode.severity || type || 'info';
}
// 呼叫內部顯示方法
return this._displayMessage(actualMessage, actualType, duration);
},
/**
* 獲取 fallback 訊息
* i18n 系統尚未載入時使用
* @param {string} code - 訊息代碼
* @param {Object} params - 參數
* @returns {string} fallback 訊息
*/
getFallbackMessage: function(code, params) {
// 基本的 fallback 訊息對照表
const fallbackMessages = {
// 系統相關
'system.connectionEstablished': 'WebSocket connection established',
'system.connectionLost': 'WebSocket connection lost',
'system.connectionReconnecting': 'Reconnecting...',
'system.connectionReconnected': 'Reconnected',
'system.connectionFailed': 'Connection failed',
'system.websocketError': 'WebSocket error',
'system.websocketReady': 'WebSocket ready',
'system.memoryPressure': 'Memory pressure cleanup',
'system.shutdown': 'System shutdown',
'system.processKilled': 'Process killed',
'system.heartbeatStopped': 'Heartbeat stopped',
// 會話相關
'session.noActiveSession': 'No active session',
'session.created': 'New session created',
'session.updated': 'Session updated',
'session.expired': 'Session expired',
'session.timeout': 'Session timed out',
'session.cleaned': 'Session cleaned',
'session.feedbackSubmitted': 'Feedback submitted successfully',
'session.userMessageRecorded': 'User message recorded',
'session.historySaved': 'Session history saved',
'session.historyLoaded': 'Session history loaded',
// 設定相關
'settings.saved': 'Settings saved',
'settings.loaded': 'Settings loaded',
'settings.cleared': 'Settings cleared',
'settings.saveFailed': 'Save failed',
'settings.loadFailed': 'Load failed',
'settings.clearFailed': 'Clear failed',
'settings.setFailed': 'Set failed',
'settings.logLevelUpdated': 'Log level updated',
'settings.invalidLogLevel': 'Invalid log level',
// 錯誤相關
'error.generic': 'An error occurred',
'error.userMessageFailed': 'Failed to add user message',
'error.getSessionsFailed': 'Failed to get sessions',
'error.getLogLevelFailed': 'Failed to get log level',
'error.command': 'Command execution error',
'error.resourceCleanup': 'Resource cleanup error',
'error.processing': 'Processing error',
// 通知相關
'notification.autoplayBlocked': 'Browser blocked autoplay, click page to enable sound',
// 預設訊息
'default': 'System message'
};
// 嘗試獲取對應的 fallback 訊息
let message = fallbackMessages[code] || fallbackMessages['default'];
// 處理參數替換(簡單版本)
if (params && typeof params === 'object') {
for (const key in params) {
if (params.hasOwnProperty(key)) {
const placeholder = '{{' + key + '}}';
message = message.replace(placeholder, params[key]);
}
}
}
// 在開發模式下顯示警告
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
console.warn('[i18n] Fallback message used for:', code, '→', message);
}
return message;
},
/**
* 內部方法實際顯示訊息
* @private
*/
_displayMessage: function(message, type, duration) {
type = type || 'info';
duration = duration || 3000;
@ -308,20 +420,7 @@
return 'WebSocket' in window;
},
/**
* 檢查 localStorage 是否可用
* @returns {boolean} localStorage 是否可用
*/
isLocalStorageSupported: function() {
try {
const test = '__localStorage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
},
/**
* HTML 轉義函數

View File

@ -47,7 +47,7 @@
'waiting_for_feedback': 'connectionMonitor.waiting',
'active': 'status.processing.title',
'feedback_submitted': 'status.submitted.title',
'completed': 'status.submitted.title',
'completed': 'status.completed.title',
'timeout': 'session.timeout',
'error': 'status.error',
'expired': 'session.timeout',

View File

@ -47,6 +47,15 @@
// 網路狀態檢測
this.networkOnline = navigator.onLine;
this.setupNetworkStatusDetection();
// 會話超時計時器
this.sessionTimeoutTimer = null;
this.sessionTimeoutInterval = null; // 用於更新倒數顯示
this.sessionTimeoutRemaining = 0; // 剩餘秒數
this.sessionTimeoutSettings = {
enabled: false,
seconds: 3600
};
}
/**
@ -74,7 +83,10 @@
this.websocket = null;
}
this.websocket = new WebSocket(wsUrl);
// 添加語言參數到 WebSocket URL
const language = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
const wsUrlWithLang = wsUrl + (wsUrl.includes('?') ? '&' : '?') + 'lang=' + language;
this.websocket = new WebSocket(wsUrlWithLang);
this.setupWebSocketEvents();
} catch (error) {
@ -261,6 +273,11 @@
console.log('WebSocket 連接確認');
this.connectionReady = true;
this.handleConnectionReady();
// 處理訊息代碼
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode);
Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_SUCCESS);
}
break;
case 'heartbeat_response':
this.handleHeartbeatResponse();
@ -269,6 +286,20 @@
this.connectionMonitor.recordPong();
}
break;
case 'ping':
// 處理來自伺服器的 ping 消息(用於連接檢測)
console.log('收到伺服器 ping立即回應 pong');
this.send({
type: 'pong',
timestamp: data.timestamp
});
break;
case 'update_timeout_settings':
// 處理超時設定更新
if (data.settings) {
this.updateSessionTimeoutSettings(data.settings);
}
break;
default:
// 其他訊息類型由外部處理
break;
@ -445,11 +476,139 @@
return true;
};
/**
* 更新會話超時設定
*/
WebSocketManager.prototype.updateSessionTimeoutSettings = function(settings) {
this.sessionTimeoutSettings = settings;
console.log('會話超時設定已更新:', settings);
// 重新啟動計時器
if (settings.enabled) {
this.startSessionTimeout();
} else {
this.stopSessionTimeout();
}
};
/**
* 啟動會話超時計時器
*/
WebSocketManager.prototype.startSessionTimeout = function() {
// 先停止現有計時器
this.stopSessionTimeout();
if (!this.sessionTimeoutSettings.enabled) {
return;
}
const timeoutSeconds = this.sessionTimeoutSettings.seconds;
this.sessionTimeoutRemaining = timeoutSeconds;
console.log('啟動會話超時計時器:', timeoutSeconds, '秒');
// 顯示倒數計時器
const displayElement = document.getElementById('sessionTimeoutDisplay');
if (displayElement) {
displayElement.style.display = '';
}
const self = this;
// 更新倒數顯示
function updateDisplay() {
const minutes = Math.floor(self.sessionTimeoutRemaining / 60);
const seconds = self.sessionTimeoutRemaining % 60;
const displayText = minutes.toString().padStart(2, '0') + ':' +
seconds.toString().padStart(2, '0');
const timerElement = document.getElementById('sessionTimeoutTimer');
if (timerElement) {
timerElement.textContent = displayText;
}
// 當剩餘時間少於60秒時改變顯示樣式
if (self.sessionTimeoutRemaining < 60 && displayElement) {
displayElement.classList.add('countdown-warning');
}
}
// 立即更新一次顯示
updateDisplay();
// 每秒更新倒數
this.sessionTimeoutInterval = setInterval(function() {
self.sessionTimeoutRemaining--;
updateDisplay();
if (self.sessionTimeoutRemaining <= 0) {
clearInterval(self.sessionTimeoutInterval);
self.sessionTimeoutInterval = null;
console.log('會話超時,準備關閉程序');
// 發送超時通知給後端
if (self.isConnected) {
self.send({
type: 'user_timeout',
timestamp: Date.now()
});
}
// 顯示超時訊息
const timeoutMessage = window.i18nManager ?
window.i18nManager.t('sessionTimeout.triggered', '會話已超時,程序即將關閉') :
'會話已超時,程序即將關閉';
Utils.showMessage(timeoutMessage, Utils.CONSTANTS.MESSAGE_WARNING);
// 延遲關閉,讓用戶看到訊息
setTimeout(function() {
window.close();
}, 3000);
}
}, 1000);
};
/**
* 停止會話超時計時器
*/
WebSocketManager.prototype.stopSessionTimeout = function() {
if (this.sessionTimeoutTimer) {
clearTimeout(this.sessionTimeoutTimer);
this.sessionTimeoutTimer = null;
}
if (this.sessionTimeoutInterval) {
clearInterval(this.sessionTimeoutInterval);
this.sessionTimeoutInterval = null;
}
// 隱藏倒數顯示
const displayElement = document.getElementById('sessionTimeoutDisplay');
if (displayElement) {
displayElement.style.display = 'none';
displayElement.classList.remove('countdown-warning');
}
console.log('會話超時計時器已停止');
};
/**
* 重置會話超時計時器用戶有活動時調用
*/
WebSocketManager.prototype.resetSessionTimeout = function() {
if (this.sessionTimeoutSettings.enabled) {
console.log('重置會話超時計時器');
this.startSessionTimeout();
}
};
/**
* 關閉連接
*/
WebSocketManager.prototype.close = function() {
this.stopHeartbeat();
this.stopSessionTimeout();
if (this.websocket) {
this.websocket.close();
this.websocket = null;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,20 @@
<!DOCTYPE html>
<html lang="zh-TW" id="html-root">
<html lang="zh-CN" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/static/icon.svg">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///wAAAAA">
<link rel="apple-touch-icon" href="/static/icon.svg">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/session-management.css">
<link rel="stylesheet" href="/static/css/prompt-management.css">
<link rel="stylesheet" href="/static/css/audio-management.css">
<link rel="stylesheet" href="/static/css/notification-settings.css">
<style>
/* 僅保留必要的頁面特定樣式和響應式調整 */
@ -390,85 +397,67 @@
</style>
</head>
<body class="layout-{{ layout_mode }}">
<!-- ===== 頂部連線監控狀態列 ===== -->
<div class="connection-monitor-bar">
<!-- 左側:應用標題和專案資訊 -->
<div class="app-info-section">
<div class="app-title">
<h1 data-i18n="app.title">MCP Feedback Enhanced</h1>
</div>
<div class="project-info">
📂 <span data-i18n="app.projectDirectory">專案目錄</span>:
<span id="projectPathDisplay" class="project-path-display"
data-full-path="{{ project_directory }}"
data-i18n-title="app.clickToCopyPath"
title="點擊複製完整路徑">{{ project_directory }}</span>
</div>
<!-- ===== 頂部連線監控狀態列(緊湊版) ===== -->
<div class="connection-monitor-bar compact">
<!-- 標題 -->
<div class="app-title-compact">
<span data-i18n="app.title">MCP Feedback</span>
</div>
<!-- 中間:連線狀態資訊 -->
<div class="connection-status-group">
<!-- 左側:會話狀態資訊 -->
<div class="session-status-info">
<div class="current-session-info">
<span class="session-indicator">
📋 <span data-i18n="sessionManagement.currentSession">當前會話</span>:
<span id="currentSessionId" class="session-id-display"
data-full-id="{{ session_id if session_id else 'loading' }}"
data-i18n-title="app.clickToCopySessionId"
title="點擊複製完整會話ID">{{ session_id[:8] if session_id else 'loading' }}...</span>
</span>
<span class="session-age">
<span data-i18n="sessionManagement.activeTime">活躍時間</span>: <span id="sessionAge">--</span>
</span>
</div>
</div>
<!-- 分隔符 -->
<span class="info-separator">·</span>
<!-- 倒數計時器顯示 -->
<div id="countdownDisplay" class="countdown-display" style="display: none;">
<span class="countdown-label" data-i18n="autoSubmit.countdownLabel">提交倒數</span>
<span id="countdownTimer" class="countdown-timer">--:--</span>
</div>
<!-- 主要連線狀態 -->
<div class="connection-indicator connecting" id="mainConnectionStatus">
<div class="status-icon pulse"></div>
<span class="status-text" data-i18n="connectionMonitor.connecting">連接中...</span>
<div class="connection-quality">
<div class="latency-indicator"><span data-i18n="connectionMonitor.latency">延遲</span>: --ms</div>
<div class="signal-strength">
<div class="signal-bar"></div>
<div class="signal-bar"></div>
<div class="signal-bar"></div>
</div>
</div>
</div>
<!-- 連線和狀態資訊組合 -->
<div class="connection-status-combined">
<!-- 連線詳細資訊 -->
<div class="connection-details">
<span class="connection-time"><span data-i18n="connectionMonitor.connectionTime">連線時間</span>: --:--</span>
<span class="reconnect-count"><span data-i18n="connectionMonitor.reconnectCount">重連</span>: 0 <span data-i18n="connectionMonitor.times"></span></span>
</div>
<!-- 詳細狀態資訊 -->
<div class="detailed-status-info">
<div class="websocket-metrics">
<span class="metric"><span data-i18n="connectionMonitor.metrics.messages">訊息</span>: <span id="messageCount">0</span></span>
<span class="metric"><span data-i18n="connectionMonitor.metrics.latencyMs">延遲</span>: <span id="latencyDisplay">--ms</span></span>
</div>
<div class="session-metrics">
<span class="metric"><span data-i18n="connectionMonitor.metrics.sessions">會話</span>: <span id="sessionCount">1</span></span>
<span class="metric"><span data-i18n="connectionMonitor.statusText">狀態</span>: <span id="sessionStatusText" data-i18n="connectionMonitor.waiting">等待中</span></span>
</div>
</div>
</div>
<!-- 專案路徑 -->
<div class="project-info-compact">
<span>📂</span>
<span id="projectPathDisplay" class="project-path-display"
data-full-path="{{ project_directory }}"
data-i18n-title="app.clickToCopyPath"
title="點擊複製完整路徑">{{ project_directory[-30:] if project_directory|length > 30 else project_directory }}</span>
</div>
<!-- 右側:快速操作 -->
<div class="quick-actions">
<!-- 保留空間以保持佈局平衡 -->
<!-- 分隔符 -->
<span class="info-separator">·</span>
<!-- 會話 ID -->
<div class="session-info-compact">
<span>📋</span>
<span id="currentSessionId" class="session-id-display"
data-full-id="{{ session_id if session_id else 'loading' }}"
data-i18n-title="app.clickToCopySessionId"
title="點擊複製完整會話ID">{{ session_id[:6] if session_id else '--' }}</span>
</div>
<!-- 倒數計時器(條件顯示) -->
<div id="countdownDisplay" class="countdown-display-compact auto-submit-display" style="display: none;">
<span class="info-separator">·</span>
<span>⏱️</span>
<span class="countdown-label" data-i18n="autoSubmit.countdownLabel">提交倒數:</span>
<span id="countdownTimer" class="countdown-timer">--:--</span>
<button id="countdownPauseBtn" class="countdown-control-btn"
data-i18n-title="autoSubmit.pauseCountdown"
title="暫停倒數"
aria-label="暫停/恢復倒數">
<span class="pause-icon"></span>
<span class="resume-icon" style="display: none;"></span>
</button>
</div>
<!-- 會話超時倒數(條件顯示) -->
<div id="sessionTimeoutDisplay" class="countdown-display-compact session-timeout-display" style="display: none;">
<span class="info-separator">·</span>
<span></span>
<span class="timeout-label" data-i18n="sessionTimeout.label">會話超時:</span>
<span id="sessionTimeoutTimer" class="countdown-timer">--:--</span>
</div>
<!-- 分隔符 -->
<span class="info-separator">·</span>
<!-- 連線狀態 -->
<div class="connection-status-compact" id="connectionStatusMinimal">
<span class="status-dot"></span>
<span class="status-text" data-i18n="connectionMonitor.connected">已連線</span>
</div>
</div>
@ -483,13 +472,10 @@
<!-- 分頁導航 -->
<div class="tabs">
<div class="tab-buttons">
<!-- 工作區分頁 - 移到最左邊第一個 -->
<button class="tab-button hidden" data-tab="combined" data-i18n="tabs.combined">
<!-- 工作區分頁 - 主要分頁 -->
<button class="tab-button active" data-tab="combined" data-i18n="tabs.combined">
📝 工作區
</button>
<button class="tab-button active" data-tab="feedback" data-i18n="tabs.feedback">
💬 回饋
</button>
<button class="tab-button" data-tab="summary" data-i18n="tabs.summary">
📋 AI 摘要
</button>
@ -510,58 +496,10 @@
<!-- ===== 回饋分頁 ===== -->
<div id="tab-feedback" class="tab-content active">
<div class="section-description" data-i18n="feedback.description">
請提供您對 AI 工作成果的回饋意見。您可以輸入文字回饋並上傳相關圖片。
</div>
<!-- 等待回饋狀態指示器 -->
{% set id = "feedbackStatusIndicator" %}
{% set status = "waiting" %}
{% set icon = "⏳" %}
{% set title = "等待回饋" %}
{% set message = "請提供您的回饋意見" %}
{% include 'components/status-indicator.html' %}
<div class="input-group">
<label class="input-label" data-i18n="feedback.textLabel">文字回饋</label>
<!-- 提示詞按鈕 -->
<div class="prompt-input-buttons" id="feedbackPromptButtons">
<button type="button" class="prompt-input-btn select-prompt-btn" data-container-index="0">
<span>📝</span>
<span class="button-text" data-i18n="prompts.buttons.selectPrompt">Templates</span>
</button>
<button type="button" class="prompt-input-btn last-prompt-btn" data-container-index="0">
<span>🔄</span>
<span class="button-text" data-i18n="prompts.buttons.useLastPrompt">Last Used</span>
</button>
</div>
<textarea
id="feedbackText"
class="text-input"
data-i18n-placeholder="feedback.detailedPlaceholder"
placeholder="請在這裡輸入您的回饋...
💡 小提示:
• 按 Ctrl+Enter/Cmd+Enter (支援數字鍵盤) 可快速提交
• 按 Ctrl+V/Cmd+V 可直接貼上剪貼板圖片"
></textarea>
</div>
<!-- 圖片上傳組件 -->
{% set id_prefix = "feedback" %}
{% include 'components/image-upload.html' %}
</div>
<!-- ===== AI 摘要分頁 ===== -->
<div id="tab-summary" class="tab-content">
<div class="section-description" data-i18n="summary.description">
以下是 AI 助手完成的工作摘要,請仔細查看並提供您的回饋意見。
</div>
<div class="input-group">
<div id="summaryContent" class="text-input" style="min-height: 300px; cursor: text; padding: 12px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word;" data-dynamic-content="aiSummary">
{{ summary }}
@ -576,8 +514,8 @@
<div id="commandOutput" class="command-output"></div>
</div>
<!-- 命令輸入區域 - 放在下面 -->
<div class="input-group" style="margin-bottom: 0;">
<!-- 命令輸入區域 -->
<div class="input-group" style="margin-bottom: 20px;">
<label class="input-label" data-i18n="command.inputLabel">命令輸入</label>
<div style="display: flex; gap: 10px; align-items: flex-start;">
<div style="flex: 1; display: flex; align-items: center; gap: 8px;">
@ -596,10 +534,69 @@
</button>
</div>
</div>
<!-- 自動執行命令設定 -->
<div class="command-auto-settings">
<div class="settings-header">
<h4 class="settings-title" data-i18n="command.autoCommand.title">🤖 自動執行命令設定</h4>
<label class="toggle-switch">
<input type="checkbox" id="autoCommandEnabled" class="toggle-input">
<span class="toggle-slider"></span>
</label>
</div>
<p class="settings-description" data-i18n="command.autoCommand.description">設定在特定時機自動執行的命令</p>
<div class="settings-content" id="autoCommandContent">
<!-- 新會話建立時執行 -->
<div class="auto-command-item">
<div class="command-label">
<span class="command-icon">🆕</span>
<span data-i18n="command.autoCommand.onNewSession">新會話建立時執行</span>
</div>
<div class="command-input-wrapper">
<span class="command-prefix">$</span>
<input type="text"
id="commandOnNewSession"
class="auto-command-input"
data-i18n-placeholder="command.autoCommand.onNewSessionPlaceholder"
placeholder="輸入要自動執行的命令...">
</div>
<div class="command-actions">
<button class="btn-small btn-test" id="testNewSessionCommand" data-i18n="command.autoCommand.testOnNewSession">測試新會話命令</button>
<span class="command-hint" data-i18n="command.autoSettings.exampleNewSession">💡 範例pwd, git status, ls -la</span>
</div>
</div>
<!-- 提交回饋後執行 -->
<div class="auto-command-item">
<div class="command-label">
<span class="command-icon"></span>
<span data-i18n="command.autoCommand.onFeedbackSubmit">提交回饋後執行</span>
</div>
<div class="command-input-wrapper">
<span class="command-prefix">$</span>
<input type="text"
id="commandOnFeedbackSubmit"
class="auto-command-input"
data-i18n-placeholder="command.autoCommand.onFeedbackSubmitPlaceholder"
placeholder="輸入要自動執行的命令...">
</div>
<div class="command-actions">
<button class="btn-small btn-test" id="testFeedbackCommand" data-i18n="command.autoCommand.testOnFeedbackSubmit">測試回饋提交命令</button>
<span class="command-hint" data-i18n="command.autoSettings.exampleFeedback">💡 範例date, echo "Done", git log -1</span>
</div>
</div>
<!-- 說明文字 -->
<div class="auto-command-help">
<p class="help-text" data-i18n="command.autoCommand.help">這些命令會在對應的時機自動執行。留空表示不執行任何命令。</p>
</div>
</div>
</div>
</div>
<!-- 工作區分頁 - 移動到此位置 -->
<div id="tab-combined" class="tab-content">
<!-- 工作區分頁 - 主要分頁 -->
<div id="tab-combined" class="tab-content active">
<div class="combined-content">
<!-- AI 摘要區域 -->
<div class="combined-section">
@ -620,29 +617,9 @@
</button>
</div>
<!-- 等待回饋狀態指示器 -->
{% set id = "combinedFeedbackStatusIndicator" %}
{% set status = "waiting" %}
{% set icon = "⏳" %}
{% set title = "等待回饋" %}
{% set message = "請提供您的回饋意見" %}
{% include 'components/status-indicator.html' %}
<div class="input-group">
<label class="input-label" data-i18n="feedback.textLabel">文字回饋</label>
<!-- 提示詞按鈕 -->
<div class="prompt-input-buttons" id="combinedPromptButtons">
<button type="button" class="prompt-input-btn select-prompt-btn" data-container-index="1">
<span>📝</span>
<span class="button-text" data-i18n="prompts.buttons.selectPrompt">Templates</span>
</button>
<button type="button" class="prompt-input-btn last-prompt-btn" data-container-index="1">
<span>🔄</span>
<span class="button-text" data-i18n="prompts.buttons.useLastPrompt">Last Used</span>
</button>
</div>
<textarea
id="combinedFeedbackText"
class="text-input"
@ -654,6 +631,22 @@
• 按 Ctrl+V/Cmd+V 可直接貼上剪貼板圖片"
style="min-height: 150px;"
></textarea>
<!-- 提示詞按鈕 - 移至輸入框下方 -->
<div class="prompt-input-buttons" id="combinedPromptButtons" style="margin-top: 8px;">
<button type="button" class="prompt-input-btn select-prompt-btn" data-container-index="1">
<span>📝</span>
<span class="button-text" data-i18n="prompts.buttons.selectPrompt">Templates</span>
</button>
<button type="button" class="prompt-input-btn last-prompt-btn" data-container-index="1">
<span>🔄</span>
<span class="button-text" data-i18n="prompts.buttons.useLastPrompt">Last Used</span>
</button>
<button type="button" class="prompt-input-btn copy-user-content-btn" id="copyUserFeedback">
<span>📋</span>
<span class="button-text" data-i18n="sessionManagement.copyUserContent">複製用戶內容</span>
</button>
</div>
</div>
<!-- 圖片上傳組件 -->
@ -698,13 +691,49 @@
</div>
<div class="session-actions">
<button class="btn-small" id="viewSessionDetails" data-i18n="sessionManagement.viewDetails">詳細資訊</button>
<button class="btn-small btn-primary" id="copyCurrentSessionContent"
data-i18n="sessionManagement.copySessionContent"
data-i18n-title="sessionManagement.copySessionContent"
aria-label="複製會話內容">
📋 <span data-i18n="sessionManagement.copySessionContent">複製會話內容</span>
</button>
<button class="btn-small btn-secondary" id="copyCurrentUserContent"
data-i18n="sessionManagement.copyUserContent"
data-i18n-title="sessionManagement.copyUserContent"
aria-label="複製用戶內容">
📝 <span data-i18n="sessionManagement.copyUserContent">複製用戶內容</span>
</button>
</div>
</div>
</div>
<!-- 會話歷史記錄 -->
<div class="session-history-section">
<h4 data-i18n="sessionManagement.sessionHistory">會話歷史</h4>
<div class="session-history-header">
<h4 data-i18n="sessionManagement.sessionHistory">會話歷史</h4>
<div class="session-history-actions">
<button class="btn-small btn-secondary" id="sessionTabExportAllBtn"
data-i18n="sessionHistory.management.exportAll"
data-i18n-title="sessionHistory.management.exportAllTitle"
aria-label="匯出全部會話">
匯出全部
</button>
<button class="btn-small btn-secondary" id="sessionTabClearMessagesBtn"
style="color: var(--warning-color);"
data-i18n="sessionHistory.userMessages.clearAll"
data-i18n-title="sessionHistory.userMessages.clearAllTitle"
aria-label="清空訊息記錄">
清空訊息記錄
</button>
<button class="btn-small btn-secondary" id="sessionTabClearAllBtn"
style="color: var(--error-color);"
data-i18n="sessionHistory.management.clear"
data-i18n-title="sessionHistory.management.clearTitle"
aria-label="清空所有會話">
清空
</button>
</div>
</div>
<div class="session-list" id="sessionHistoryList">
<div class="no-sessions" data-i18n="sessionManagement.noHistory">暫無歷史會話</div>
</div>
@ -732,13 +761,13 @@
<!-- 介面設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="settings.interface">🎨 介面設定</h3>
<h3 class="settings-card-title" data-i18n="settingsUI.interface">🎨 介面設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="settings.layoutMode">界面佈局模式</div>
<div class="setting-description" data-i18n="settings.layoutModeDesc">
<div class="setting-label" data-i18n="settingsUI.layoutMode">界面佈局模式</div>
<div class="setting-description" data-i18n="settingsUI.layoutModeDesc">
選擇 AI 摘要和回饋輸入的顯示方式
</div>
</div>
@ -746,15 +775,15 @@
<div class="layout-option">
<input type="radio" id="combinedVertical" name="layoutMode" value="combined-vertical" checked>
<label for="combinedVertical">
<div class="layout-option-title" data-i18n="settings.combinedVertical">垂直布局</div>
<div class="layout-option-desc" data-i18n="settings.combinedVerticalDesc">AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面</div>
<div class="layout-option-title" data-i18n="settingsUI.combinedVertical">垂直布局</div>
<div class="layout-option-desc" data-i18n="settingsUI.combinedVerticalDesc">AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面</div>
</label>
</div>
<div class="layout-option">
<input type="radio" id="combinedHorizontal" name="layoutMode" value="combined-horizontal">
<label for="combinedHorizontal">
<div class="layout-option-title" data-i18n="settings.combinedHorizontal">水平布局</div>
<div class="layout-option-desc" data-i18n="settings.combinedHorizontalDesc">AI 摘要在左,回饋輸入在右,增大摘要可視區域</div>
<div class="layout-option-title" data-i18n="settingsUI.combinedHorizontal">水平布局</div>
<div class="layout-option-desc" data-i18n="settingsUI.combinedHorizontalDesc">AI 摘要在左,回饋輸入在右,增大摘要可視區域</div>
</label>
</div>
</div>
@ -768,20 +797,20 @@
<!-- 語言設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="settings.language">🌐 語言設定</h3>
<h3 class="settings-card-title" data-i18n="settingsUI.language">🌍 語言設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="settings.currentLanguage">當前語言</div>
<div class="setting-description" data-i18n="settings.languageDesc">
<div class="setting-label" data-i18n="settingsUI.currentLanguage">當前語言</div>
<div class="setting-description" data-i18n="settingsUI.languageDesc">
選擇界面顯示語言
</div>
</div>
<div class="language-selector-dropdown">
<select id="settingsLanguageSelect" class="language-setting-select">
<option value="zh-TW" data-i18n="languages.zh-TW">繁體中文</option>
<option value="zh-CN" data-i18n="languages.zh-CN">简体中文</option>
<option value="zh-TW" data-i18n="languages.zh-TW">繁體中文</option>
<option value="en" data-i18n="languages.en">English</option>
</select>
</div>
@ -843,7 +872,7 @@
</div>
</div>
<div class="setting-control">
<button type="button" id="autoSubmitToggle" class="toggle-btn" aria-label="切換自動提交">
<button type="button" id="autoSubmitToggle" class="toggle-btn" data-i18n-aria-label="aria.toggleAutoSubmit">
<span class="toggle-slider"></span>
</button>
</div>
@ -892,13 +921,57 @@
</div>
</div>
<!-- 音效通知設定卡片 -->
<!-- 音效通知設定 -->
<div id="audioManagementContainer">
<!-- 音效管理 UI 將在這裡動態生成 -->
</div>
<!-- 瀏覽器通知設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="audio.notification.title">🔊 音效通知設定</h3>
<h3 class="settings-card-title">
<span data-i18n="notification.title">🔔 瀏覽器通知</span>
</h3>
</div>
<div class="settings-card-body" id="audioManagementContainer">
<!-- 音效管理 UI 將在這裡動態生成 -->
<div class="settings-card-body" id="notificationSettingsContainer">
<!-- 通知設定 UI 將在這裡動態生成 -->
</div>
</div>
<!-- 會話超時設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="settingsUI.sessionTimeoutTitle">⏱️ 會話超時設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="settingsUI.sessionTimeoutEnable">啟用會話超時</div>
<div class="setting-description" data-i18n="settingsUI.sessionTimeoutEnableDesc">
啟用後,會話將在指定時間後自動關閉
</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="sessionTimeoutEnabled" class="toggle-input">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="settingsUI.sessionTimeoutDuration">超時時間(秒)</div>
<div class="setting-description" data-i18n="settingsUI.sessionTimeoutDurationDesc">
設定會話超時時間範圍300-86400 秒5分鐘-24小時
</div>
</div>
<div class="setting-control">
<input type="number" id="sessionTimeoutSeconds" min="300" max="86400" value="3600"
class="form-input" style="width: 120px;">
<span class="input-suffix" data-i18n="settingsUI.sessionTimeoutSeconds"></span>
</div>
</div>
</div>
</div>
@ -994,18 +1067,18 @@
<!-- 重置設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="settings.advanced">🔧 進階設定</h3>
<h3 class="settings-card-title" data-i18n="settingsUI.advanced">🔧 進階設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item" style="border-bottom: none;">
<div class="setting-info">
<div class="setting-label" data-i18n="settings.reset">重置設定</div>
<div class="setting-description" data-i18n="settings.resetDesc">
<div class="setting-label" data-i18n="settingsUI.reset">重置設定</div>
<div class="setting-description" data-i18n="settingsUI.resetDesc">
清除所有已保存的設定,恢復到預設狀態
</div>
</div>
<button id="resetSettingsBtn" class="btn btn-secondary" style="font-size: 12px; padding: 6px 16px;">
<span data-i18n="settings.reset">重置設定</span>
<span data-i18n="settingsUI.reset">重置設定</span>
</button>
</div>
</div>
@ -1027,7 +1100,6 @@
<div class="setting-item" style="border-bottom: none; padding-bottom: 16px;">
<div class="setting-info">
<div class="setting-description" data-i18n="about.description" style="color: var(--text-secondary); font-size: 13px; line-height: 1.5;">
一個強大的 MCP 伺服器,為 AI 輔助開發工具提供人在回路的互動回饋功能。支援 Web UI 介面,並具備圖片上傳、命令執行、多語言等豐富功能。
</div>
</div>
</div>
@ -1096,9 +1168,9 @@
</div>
<!-- WebSocket 和 JavaScript -->
<!-- Markdown 支援庫 -->
<script src="https://cdn.jsdelivr.net/npm/marked@14.1.3/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.2/dist/purify.min.js"></script>
<!-- Markdown 支援庫 - 本地版本 -->
<script src="/static/js/vendor/marked.min.js"></script>
<script src="/static/js/vendor/purify.min.js"></script>
<script src="/static/js/i18n.js?v=2025010510"></script>
<!-- 載入所有模組 -->
<!-- 核心模組(最先載入) -->
@ -1124,6 +1196,10 @@
<script src="/static/js/modules/audio/audio-manager.js?v=2025010510"></script>
<script src="/static/js/modules/audio/audio-settings-ui.js?v=2025010510"></script>
<!-- 通知模組 -->
<script src="/static/js/modules/notification/notification-manager.js?v=2025010510"></script>
<script src="/static/js/modules/notification/notification-settings.js?v=2025010510"></script>
<!-- 其他模組 -->
<script src="/static/js/modules/utils.js?v=2025010510"></script>
<script src="/static/js/modules/tab-manager.js?v=2025010510"></script>
@ -1232,5 +1308,53 @@
initializeApp();
}
</script>
<!-- 可折疊統計面板 -->
<div class="stats-panel-floating collapsed" id="statsPanel">
<div class="stats-panel-header" onclick="toggleStatsPanel()">
<div class="stats-panel-title">
<span>📊</span>
<span data-i18n="stats.detailedStats">詳細統計資訊</span>
</div>
<span class="stats-toggle-icon"></span>
</div>
<div class="stats-panel-content">
<!-- 連線資訊 -->
<div class="stats-item-detailed">
<span class="stats-item-label" data-i18n="connectionMonitor.connectionTime">連線時間</span>
<span class="stats-item-value" id="statsConnectionTime">--:--</span>
</div>
<div class="stats-item-detailed">
<span class="stats-item-label" data-i18n="connectionMonitor.reconnectCount">重連次數</span>
<span class="stats-item-value" id="statsReconnectCount">0</span>
</div>
<!-- WebSocket 統計 -->
<div class="stats-item-detailed">
<span class="stats-item-label" data-i18n="connectionMonitor.metrics.messages">訊息數</span>
<span class="stats-item-value" id="statsMessageCount">0</span>
</div>
<div class="stats-item-detailed">
<span class="stats-item-label" data-i18n="connectionMonitor.metrics.latencyMs">延遲</span>
<span class="stats-item-value" id="statsLatency">--ms</span>
</div>
<!-- 會話統計 -->
<div class="stats-item-detailed">
<span class="stats-item-label" data-i18n="connectionMonitor.metrics.sessions">會話數</span>
<span class="stats-item-value" id="statsSessionCount">1</span>
</div>
<div class="stats-item-detailed">
<span class="stats-item-label" data-i18n="connectionMonitor.statusText">狀態</span>
<span class="stats-item-value" id="statsSessionStatus" data-i18n="connectionMonitor.waiting">等待中</span>
</div>
</div>
</div>
<script>
// 統計面板切換功能
function toggleStatsPanel() {
const panel = document.getElementById('statsPanel');
panel.classList.toggle('collapsed');
}
</script>
</body>
</html>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh-TW" id="html-root">
<html lang="zh-CN" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -129,7 +129,6 @@
/* 主容器 - 有會話時顯示 */
.main-container {
display: none;
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;
@ -319,7 +318,7 @@
<script src="/static/js/i18n.js?v=2025010505"></script>
<!-- 載入所有模組 -->
<script src="/static/js/modules/utils.js?v=2025010505"></script>
<script src="/static/js/modules/tab-manager.js?v=2025010505"></script>
<script src="/static/js/modules/websocket-manager.js?v=2025010505"></script>
<script src="/static/js/modules/image-handler.js?v=2025010505"></script>
<script src="/static/js/modules/settings-manager.js?v=2025010505"></script>

View File

@ -54,14 +54,16 @@ def web_ui_manager() -> Generator[WebUIManager, None, None]:
# 設置測試模式環境變數
original_test_mode = os.environ.get("MCP_TEST_MODE")
original_web_host = os.environ.get("MCP_WEB_HOST")
original_web_port = os.environ.get("MCP_WEB_PORT")
os.environ["MCP_TEST_MODE"] = "true"
os.environ["MCP_WEB_HOST"] = "127.0.0.1" # 確保測試使用本地主機
# 使用動態端口範圍避免衝突
os.environ["MCP_WEB_PORT"] = "0" # 讓系統自動分配端口
try:
manager = WebUIManager(host="127.0.0.1") # 使用環境變數控制端口
manager = WebUIManager() # 使用環境變數控制主機和端口
yield manager
finally:
# 恢復原始環境變數
@ -70,6 +72,11 @@ def web_ui_manager() -> Generator[WebUIManager, None, None]:
else:
os.environ.pop("MCP_TEST_MODE", None)
if original_web_host is not None:
os.environ["MCP_WEB_HOST"] = original_web_host
else:
os.environ.pop("MCP_WEB_HOST", None)
if original_web_port is not None:
os.environ["MCP_WEB_PORT"] = original_web_port
else:

View File

@ -182,8 +182,8 @@ class TestWebFeedbackSessionCleanup:
"""測試狀態更新重置定時器"""
old_timer = self.session.cleanup_timer
# 更新狀態為活躍
self.session.update_status(SessionStatus.ACTIVE, "測試活躍狀態")
# 更新狀態為活躍 - 使用 next_step 方法
self.session.next_step("測試活躍狀態")
# 檢查定時器是否被重置
assert self.session.cleanup_timer != old_timer

View File

@ -108,11 +108,16 @@ class TestWebFeedbackSession:
# 測試初始狀態
assert session.status == SessionStatus.WAITING
# 測試狀態更新
session.update_status(SessionStatus.FEEDBACK_SUBMITTED, "已提交回饋")
# 測試狀態更新 - 使用 next_step 方法
# 首先進入 ACTIVE 狀態
result = session.next_step("會話已激活")
assert result is True
assert session.status == SessionStatus.ACTIVE
# 然後進入 FEEDBACK_SUBMITTED 狀態
result = session.next_step("已提交回饋") # type: ignore[unreachable]
assert result is True
assert session.status == SessionStatus.FEEDBACK_SUBMITTED
# 修復 unreachable 錯誤 - 使用 type: ignore 註解
assert session.status_message == "已提交回饋" # type: ignore[unreachable]
assert session.status_message == "已提交回饋"
def test_session_age_and_idle_time(self, test_project_dir):
"""測試會話年齡和空閒時間"""