cloneloop 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloneloop-0.1.0.dist-info/METADATA +392 -0
- cloneloop-0.1.0.dist-info/RECORD +118 -0
- cloneloop-0.1.0.dist-info/WHEEL +4 -0
- cloneloop-0.1.0.dist-info/entry_points.txt +5 -0
- mcp_ai_supervisor/__init__.py +52 -0
- mcp_ai_supervisor/__main__.py +551 -0
- mcp_ai_supervisor/debug.py +15 -0
- mcp_ai_supervisor/desktop_app/__init__.py +29 -0
- mcp_ai_supervisor/desktop_app/desktop_app.py +390 -0
- mcp_ai_supervisor/helpers/__init__.py +4 -0
- mcp_ai_supervisor/helpers/feedback_helpers.py +140 -0
- mcp_ai_supervisor/helpers/image_helpers.py +73 -0
- mcp_ai_supervisor/i18n.py +14 -0
- mcp_ai_supervisor/log_writer.py +16 -0
- mcp_ai_supervisor/logging/__init__.py +8 -0
- mcp_ai_supervisor/logging/log_parser.py +293 -0
- mcp_ai_supervisor/logging/log_tailer.py +367 -0
- mcp_ai_supervisor/py.typed +0 -0
- mcp_ai_supervisor/server.py +654 -0
- mcp_ai_supervisor/utils/__init__.py +28 -0
- mcp_ai_supervisor/utils/error_handler.py +10 -0
- mcp_ai_supervisor/utils/memory_monitor.py +11 -0
- mcp_ai_supervisor/utils/paths.py +7 -0
- mcp_ai_supervisor/utils/resource_manager.py +15 -0
- mcp_ai_supervisor/utils/structure_checker.py +291 -0
- mcp_ai_supervisor/web/__init__.py +26 -0
- mcp_ai_supervisor/web/constants/__init__.py +8 -0
- mcp_ai_supervisor/web/constants/message_codes.py +173 -0
- mcp_ai_supervisor/web/core/__init__.py +30 -0
- mcp_ai_supervisor/web/core/agent_bridge.py +957 -0
- mcp_ai_supervisor/web/core/auto_responder.py +332 -0
- mcp_ai_supervisor/web/core/context_collector.py +124 -0
- mcp_ai_supervisor/web/core/conversation_recorder.py +751 -0
- mcp_ai_supervisor/web/core/delegate_manager.py +1193 -0
- mcp_ai_supervisor/web/core/feedback_mediator.py +259 -0
- mcp_ai_supervisor/web/core/message_channel.py +551 -0
- mcp_ai_supervisor/web/core/response_builder.py +333 -0
- mcp_ai_supervisor/web/core/scene_detector.py +184 -0
- mcp_ai_supervisor/web/core/task_state.py +194 -0
- mcp_ai_supervisor/web/locales/en/translation.json +643 -0
- mcp_ai_supervisor/web/locales/zh-CN/translation.json +629 -0
- mcp_ai_supervisor/web/locales/zh-TW/translation.json +648 -0
- mcp_ai_supervisor/web/main.py +747 -0
- mcp_ai_supervisor/web/managers/__init__.py +8 -0
- mcp_ai_supervisor/web/managers/desktop_manager.py +228 -0
- mcp_ai_supervisor/web/managers/server_manager.py +170 -0
- mcp_ai_supervisor/web/managers/session_manager.py +515 -0
- mcp_ai_supervisor/web/managers/workbench_connector.py +186 -0
- mcp_ai_supervisor/web/models/__init__.py +13 -0
- mcp_ai_supervisor/web/models/feedback_result.py +16 -0
- mcp_ai_supervisor/web/models/feedback_session.py +938 -0
- mcp_ai_supervisor/web/models/session_cleaner.py +245 -0
- mcp_ai_supervisor/web/models/session_commands.py +213 -0
- mcp_ai_supervisor/web/models/session_resolver.py +158 -0
- mcp_ai_supervisor/web/models/session_timer.py +242 -0
- mcp_ai_supervisor/web/routes/__init__.py +12 -0
- mcp_ai_supervisor/web/routes/main_routes.py +965 -0
- mcp_ai_supervisor/web/routes/settings_routes.py +195 -0
- mcp_ai_supervisor/web/routes/ws_handlers.py +270 -0
- mcp_ai_supervisor/web/static/css/audio-management.css +545 -0
- mcp_ai_supervisor/web/static/css/notification-settings.css +152 -0
- mcp_ai_supervisor/web/static/css/prompt-management.css +566 -0
- mcp_ai_supervisor/web/static/css/session-management.css +1428 -0
- mcp_ai_supervisor/web/static/css/styles.css +2267 -0
- mcp_ai_supervisor/web/static/favicon.ico +0 -0
- mcp_ai_supervisor/web/static/icon-192.png +0 -0
- mcp_ai_supervisor/web/static/icon.svg +11 -0
- mcp_ai_supervisor/web/static/index.html +37 -0
- mcp_ai_supervisor/web/static/js/app.js +1721 -0
- mcp_ai_supervisor/web/static/js/i18n.js +376 -0
- mcp_ai_supervisor/web/static/js/modules/audio/audio-manager.js +610 -0
- mcp_ai_supervisor/web/static/js/modules/audio/audio-settings-ui.js +732 -0
- mcp_ai_supervisor/web/static/js/modules/connection-monitor.js +435 -0
- mcp_ai_supervisor/web/static/js/modules/constants/message-codes.js +168 -0
- mcp_ai_supervisor/web/static/js/modules/countdown-manager.js +273 -0
- mcp_ai_supervisor/web/static/js/modules/drag-drop-handler.js +677 -0
- mcp_ai_supervisor/web/static/js/modules/file-upload-manager.js +555 -0
- mcp_ai_supervisor/web/static/js/modules/image-handler.js +199 -0
- mcp_ai_supervisor/web/static/js/modules/logger.js +404 -0
- mcp_ai_supervisor/web/static/js/modules/notification/notification-manager.js +360 -0
- mcp_ai_supervisor/web/static/js/modules/notification/notification-settings.js +344 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-input-buttons.js +427 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-manager.js +414 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-modal.js +458 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-settings-ui.js +524 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-data-manager.js +1042 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-details-modal.js +594 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-ui-renderer.js +836 -0
- mcp_ai_supervisor/web/static/js/modules/session-manager.js +1059 -0
- mcp_ai_supervisor/web/static/js/modules/settings-manager.js +1002 -0
- mcp_ai_supervisor/web/static/js/modules/tab-manager.js +235 -0
- mcp_ai_supervisor/web/static/js/modules/textarea-height-manager.js +267 -0
- mcp_ai_supervisor/web/static/js/modules/ui-manager.js +578 -0
- mcp_ai_supervisor/web/static/js/modules/utils/dom-utils.js +392 -0
- mcp_ai_supervisor/web/static/js/modules/utils/status-utils.js +403 -0
- mcp_ai_supervisor/web/static/js/modules/utils/time-utils.js +440 -0
- mcp_ai_supervisor/web/static/js/modules/utils.js +557 -0
- mcp_ai_supervisor/web/static/js/modules/websocket-manager.js +875 -0
- mcp_ai_supervisor/web/static/js/modules/workbench-notification.js +103 -0
- mcp_ai_supervisor/web/static/js/vendor/marked.min.js +6 -0
- mcp_ai_supervisor/web/static/js/vendor/purify.min.js +3 -0
- mcp_ai_supervisor/web/templates/components/image-upload.html +43 -0
- mcp_ai_supervisor/web/templates/components/settings-card.html +58 -0
- mcp_ai_supervisor/web/templates/components/status-indicator.html +31 -0
- mcp_ai_supervisor/web/templates/components/toggle-switch.html +19 -0
- mcp_ai_supervisor/web/templates/feedback.html +1198 -0
- mcp_ai_supervisor/web/templates/index.html +378 -0
- mcp_ai_supervisor/web/utils/__init__.py +12 -0
- mcp_ai_supervisor/web/utils/browser.py +35 -0
- mcp_ai_supervisor/web/utils/compression_config.py +195 -0
- mcp_ai_supervisor/web/utils/compression_monitor.py +314 -0
- mcp_ai_supervisor/web/utils/ide_opener.py +286 -0
- mcp_ai_supervisor/web/utils/network.py +66 -0
- mcp_ai_supervisor/web/utils/port_manager.py +340 -0
- mcp_ai_supervisor/web/utils/session_cleanup_manager.py +543 -0
- mcp_ai_supervisor/web/utils/workbench_client.py +899 -0
- mcp_ai_supervisor/workbench/__init__.py +5 -0
- mcp_ai_supervisor/workbench/__main__.py +5 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
桌面應用程式主要模組
|
|
4
|
+
|
|
5
|
+
此模組提供桌面應用程式的核心功能,包括:
|
|
6
|
+
- 桌面模式檢測
|
|
7
|
+
- Tauri 應用程式啟動
|
|
8
|
+
- 與現有 Web UI 的整合
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 導入現有的 MCP AI Supervisor 模組
|
|
18
|
+
try:
|
|
19
|
+
from mcp_ai_supervisor.debug import server_debug_log as debug_log
|
|
20
|
+
from mcp_ai_supervisor.log_writer import get_logger as _get_struct_logger
|
|
21
|
+
from mcp_ai_supervisor.web.main import WebUIManager, get_web_ui_manager
|
|
22
|
+
except ImportError as e:
|
|
23
|
+
# 在這裡無法使用 debug_log,因為導入失敗
|
|
24
|
+
sys.stderr.write(f"無法導入 MCP AI Supervisor 模組: {e}\n")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
# 桌面应用结构化日志记录器
|
|
28
|
+
_tauri_logger = _get_struct_logger("tauri")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DesktopApp:
|
|
32
|
+
"""桌面應用程式管理器"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.web_manager: WebUIManager | None = None
|
|
36
|
+
self.desktop_mode = False
|
|
37
|
+
self.app_handle = None
|
|
38
|
+
|
|
39
|
+
def set_desktop_mode(self, enabled: bool = True):
|
|
40
|
+
"""設置桌面模式"""
|
|
41
|
+
self.desktop_mode = enabled
|
|
42
|
+
if enabled:
|
|
43
|
+
# 設置環境變數,防止開啟瀏覽器
|
|
44
|
+
os.environ["MCP_DESKTOP_MODE"] = "true"
|
|
45
|
+
debug_log("桌面模式已啟用,將禁止開啟瀏覽器")
|
|
46
|
+
else:
|
|
47
|
+
os.environ.pop("MCP_DESKTOP_MODE", None)
|
|
48
|
+
debug_log("桌面模式已禁用")
|
|
49
|
+
|
|
50
|
+
def is_desktop_mode(self) -> bool:
|
|
51
|
+
"""檢查是否為桌面模式"""
|
|
52
|
+
return (
|
|
53
|
+
self.desktop_mode
|
|
54
|
+
or os.environ.get("MCP_DESKTOP_MODE", "").lower() == "true"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def start_web_backend(self) -> str:
|
|
58
|
+
"""啟動 Web 後端服務"""
|
|
59
|
+
debug_log("啟動 Web 後端服務...")
|
|
60
|
+
|
|
61
|
+
# 獲取 Web UI 管理器
|
|
62
|
+
self.web_manager = get_web_ui_manager()
|
|
63
|
+
|
|
64
|
+
# 設置桌面模式,禁止自動開啟瀏覽器
|
|
65
|
+
self.set_desktop_mode(True)
|
|
66
|
+
|
|
67
|
+
# 啟動服務器
|
|
68
|
+
if (
|
|
69
|
+
self.web_manager.server_thread is None
|
|
70
|
+
or not self.web_manager.server_thread.is_alive()
|
|
71
|
+
):
|
|
72
|
+
self.web_manager.start_server()
|
|
73
|
+
|
|
74
|
+
# 等待服務器啟動
|
|
75
|
+
max_wait = 10.0 # 最多等待 10 秒
|
|
76
|
+
wait_count = 0.0
|
|
77
|
+
while wait_count < max_wait:
|
|
78
|
+
if (
|
|
79
|
+
self.web_manager.server_thread
|
|
80
|
+
and self.web_manager.server_thread.is_alive()
|
|
81
|
+
):
|
|
82
|
+
break
|
|
83
|
+
await asyncio.sleep(0.5)
|
|
84
|
+
wait_count += 0.5
|
|
85
|
+
|
|
86
|
+
if not (
|
|
87
|
+
self.web_manager.server_thread and self.web_manager.server_thread.is_alive()
|
|
88
|
+
):
|
|
89
|
+
raise RuntimeError("Web 服務器啟動失敗")
|
|
90
|
+
|
|
91
|
+
server_url = self.web_manager.get_server_url()
|
|
92
|
+
debug_log(f"Web 後端服務已啟動: {server_url}")
|
|
93
|
+
return server_url
|
|
94
|
+
|
|
95
|
+
def create_test_session(self):
|
|
96
|
+
"""創建測試會話"""
|
|
97
|
+
if not self.web_manager:
|
|
98
|
+
raise RuntimeError("Web 管理器未初始化")
|
|
99
|
+
|
|
100
|
+
import tempfile
|
|
101
|
+
|
|
102
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
103
|
+
session_id, _ = self.web_manager.create_session(
|
|
104
|
+
temp_dir, "桌面應用程式測試 - 驗證 Tauri 整合功能"
|
|
105
|
+
)
|
|
106
|
+
debug_log(f"測試會話已創建: {session_id}")
|
|
107
|
+
return session_id
|
|
108
|
+
|
|
109
|
+
async def launch_tauri_app(self, server_url: str):
|
|
110
|
+
"""啟動 Tauri 桌面應用程式"""
|
|
111
|
+
debug_log("正在啟動 Tauri 桌面視窗...")
|
|
112
|
+
|
|
113
|
+
import os
|
|
114
|
+
import subprocess
|
|
115
|
+
from pathlib import Path
|
|
116
|
+
|
|
117
|
+
import platform
|
|
118
|
+
|
|
119
|
+
system = platform.system().lower()
|
|
120
|
+
machine = platform.machine().lower()
|
|
121
|
+
|
|
122
|
+
def _get_binary_name() -> str:
|
|
123
|
+
if system == "windows":
|
|
124
|
+
return "mcp-ai-supervisor-desktop.exe"
|
|
125
|
+
elif system == "darwin":
|
|
126
|
+
if machine in ["arm64", "aarch64"]:
|
|
127
|
+
return "mcp-ai-supervisor-desktop-macos-arm64"
|
|
128
|
+
return "mcp-ai-supervisor-desktop-macos-intel"
|
|
129
|
+
elif system == "linux":
|
|
130
|
+
return "mcp-ai-supervisor-desktop-linux"
|
|
131
|
+
return "mcp-ai-supervisor-desktop"
|
|
132
|
+
|
|
133
|
+
def _find_project_root() -> Path:
|
|
134
|
+
"""向上查找包含 pyproject.toml 和 packages/ 的项目根目录"""
|
|
135
|
+
p = Path(__file__).resolve().parent
|
|
136
|
+
for _ in range(10):
|
|
137
|
+
if (p / "pyproject.toml").exists() and (p / "packages").is_dir():
|
|
138
|
+
return p
|
|
139
|
+
p = p.parent
|
|
140
|
+
return Path(__file__).resolve().parent.parent.parent.parent
|
|
141
|
+
|
|
142
|
+
binary_name = _get_binary_name()
|
|
143
|
+
tauri_exe = None
|
|
144
|
+
project_root = _find_project_root()
|
|
145
|
+
|
|
146
|
+
search_dirs = []
|
|
147
|
+
|
|
148
|
+
# 1. 从 Python 包内的 desktop_release 查找(pip 安装场景)
|
|
149
|
+
try:
|
|
150
|
+
from mcp_ai_supervisor.desktop_release import __file__ as desktop_init
|
|
151
|
+
search_dirs.append(Path(desktop_init).parent)
|
|
152
|
+
except ImportError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# 2. artifacts/desktop/(monorepo 构建产物标准位置)
|
|
156
|
+
search_dirs.append(project_root / "artifacts" / "desktop")
|
|
157
|
+
|
|
158
|
+
for search_dir in search_dirs:
|
|
159
|
+
candidate = search_dir / binary_name
|
|
160
|
+
if candidate.exists():
|
|
161
|
+
tauri_exe = candidate
|
|
162
|
+
debug_log(f"找到 Tauri 可執行檔案: {tauri_exe}")
|
|
163
|
+
break
|
|
164
|
+
# 尝试该目录下的所有可执行文件作为 fallback
|
|
165
|
+
for name in [
|
|
166
|
+
"mcp-ai-supervisor-desktop.exe",
|
|
167
|
+
"mcp-ai-supervisor-desktop-macos-arm64",
|
|
168
|
+
"mcp-ai-supervisor-desktop-macos-intel",
|
|
169
|
+
"mcp-ai-supervisor-desktop-linux",
|
|
170
|
+
"mcp-ai-supervisor-desktop",
|
|
171
|
+
]:
|
|
172
|
+
candidate = search_dir / name
|
|
173
|
+
if candidate.exists():
|
|
174
|
+
tauri_exe = candidate
|
|
175
|
+
debug_log(f"使用回退的可執行檔案: {tauri_exe}")
|
|
176
|
+
break
|
|
177
|
+
if tauri_exe:
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
if not tauri_exe:
|
|
181
|
+
# 最後嘗試 Tauri 開發構建路徑
|
|
182
|
+
debug_log("未找到預編譯二進制,嘗試開發環境路徑...")
|
|
183
|
+
tauri_exe = (
|
|
184
|
+
project_root
|
|
185
|
+
/ "packages"
|
|
186
|
+
/ "desktop"
|
|
187
|
+
/ "target"
|
|
188
|
+
/ "debug"
|
|
189
|
+
/ binary_name
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if not tauri_exe.exists():
|
|
193
|
+
# 嘗試 release 版本
|
|
194
|
+
tauri_exe = (
|
|
195
|
+
project_root
|
|
196
|
+
/ "src-tauri"
|
|
197
|
+
/ "target"
|
|
198
|
+
/ "release"
|
|
199
|
+
/ "mcp-ai-supervisor-desktop.exe"
|
|
200
|
+
)
|
|
201
|
+
if not tauri_exe.exists():
|
|
202
|
+
tauri_exe = (
|
|
203
|
+
project_root
|
|
204
|
+
/ "src-tauri"
|
|
205
|
+
/ "target"
|
|
206
|
+
/ "release"
|
|
207
|
+
/ "mcp-ai-supervisor-desktop"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if not tauri_exe.exists():
|
|
211
|
+
raise FileNotFoundError(
|
|
212
|
+
"找不到 Tauri 可執行檔案,已嘗試的路徑包括開發和發布目錄"
|
|
213
|
+
) from None
|
|
214
|
+
|
|
215
|
+
debug_log(f"找到 Tauri 可執行檔案: {tauri_exe}")
|
|
216
|
+
|
|
217
|
+
env = os.environ.copy()
|
|
218
|
+
env["MCP_DESKTOP_MODE"] = "true"
|
|
219
|
+
session_id = os.environ.get("MCP_SESSION_ID", "")
|
|
220
|
+
project_dir = os.environ.get("MCP_PROJECT_DIR", "")
|
|
221
|
+
|
|
222
|
+
import urllib.parse
|
|
223
|
+
web_url = server_url
|
|
224
|
+
params = []
|
|
225
|
+
if project_dir:
|
|
226
|
+
params.append(f"project={urllib.parse.quote(project_dir, safe='')}")
|
|
227
|
+
env["MCP_PROJECT_DIR"] = project_dir
|
|
228
|
+
if session_id:
|
|
229
|
+
params.append(f"session_id={session_id}")
|
|
230
|
+
env["MCP_SESSION_ID"] = session_id
|
|
231
|
+
if params:
|
|
232
|
+
web_url = f"{server_url}?{'&'.join(params)}"
|
|
233
|
+
env["MCP_WEB_URL"] = web_url
|
|
234
|
+
|
|
235
|
+
debug_log(
|
|
236
|
+
f"[Tauri-Process] ━━━ 准备启动 Tauri 进程 ━━━\n"
|
|
237
|
+
f" MCP PID={os.getpid()}\n"
|
|
238
|
+
f" MCP_WEB_URL={web_url}\n"
|
|
239
|
+
f" MCP_SESSION_ID={session_id or '(未设置)'}\n"
|
|
240
|
+
f" MCP_PROJECT_DIR={project_dir or '(未设置)'}\n"
|
|
241
|
+
f" 已有 app_handle={self.app_handle is not None}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# 啟動 Tauri 應用程式
|
|
245
|
+
try:
|
|
246
|
+
# Windows 下隱藏控制台視窗
|
|
247
|
+
creation_flags = 0
|
|
248
|
+
if os.name == "nt":
|
|
249
|
+
# CREATE_NO_WINDOW 只在 Windows 上存在
|
|
250
|
+
creation_flags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
|
|
251
|
+
|
|
252
|
+
self.app_handle = subprocess.Popen(
|
|
253
|
+
[str(tauri_exe)],
|
|
254
|
+
env=env,
|
|
255
|
+
stdout=subprocess.PIPE,
|
|
256
|
+
stderr=subprocess.PIPE,
|
|
257
|
+
creationflags=creation_flags,
|
|
258
|
+
)
|
|
259
|
+
# [TAURI_LAUNCH] 弹窗启动
|
|
260
|
+
_tauri_logger.info(
|
|
261
|
+
f"pid={self.app_handle.pid} url={web_url} project={project_dir or '(未设置)'}",
|
|
262
|
+
"[TAURI_LAUNCH] Tauri 桌面应用已启动",
|
|
263
|
+
)
|
|
264
|
+
debug_log(
|
|
265
|
+
f"[Tauri-Process] Tauri 桌面应用已启动\n"
|
|
266
|
+
f" Tauri PID={self.app_handle.pid}\n"
|
|
267
|
+
f" MCP PID={os.getpid()}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# 等待一下確保應用程式啟動
|
|
271
|
+
await asyncio.sleep(2)
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
# [TAURI_LAUNCH_FAIL] 弹窗启动失败
|
|
275
|
+
_tauri_logger.error(
|
|
276
|
+
f"project={project_dir or '(未设置)'} error={type(e).__name__}: {e}",
|
|
277
|
+
"[TAURI_LAUNCH_FAIL] Tauri 桌面应用启动失败",
|
|
278
|
+
exc_info=True,
|
|
279
|
+
)
|
|
280
|
+
debug_log(f"啟動 Tauri 應用程式失敗: {e}")
|
|
281
|
+
raise
|
|
282
|
+
|
|
283
|
+
def stop(self):
|
|
284
|
+
"""停止桌面應用程式"""
|
|
285
|
+
tauri_pid = self.app_handle.pid if self.app_handle else "(无)"
|
|
286
|
+
debug_log(
|
|
287
|
+
f"[Tauri-Process] ━━━ 停止桌面应用 ━━━\n"
|
|
288
|
+
f" Tauri PID={tauri_pid}\n"
|
|
289
|
+
f" MCP PID={os.getpid()}"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# 停止 Tauri 應用程式
|
|
293
|
+
if self.app_handle:
|
|
294
|
+
try:
|
|
295
|
+
if self.app_handle.poll() is None:
|
|
296
|
+
self.app_handle.terminate()
|
|
297
|
+
self.app_handle.wait(timeout=3)
|
|
298
|
+
debug_log("Tauri 應用程式已停止")
|
|
299
|
+
else:
|
|
300
|
+
debug_log("Tauri 應用程式已不在運行")
|
|
301
|
+
except Exception as e:
|
|
302
|
+
debug_log(f"停止 Tauri 應用程式時發生錯誤: {e}")
|
|
303
|
+
try:
|
|
304
|
+
self.app_handle.kill()
|
|
305
|
+
self.app_handle.wait(timeout=2)
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
finally:
|
|
309
|
+
self.app_handle = None
|
|
310
|
+
|
|
311
|
+
if self.web_manager:
|
|
312
|
+
# 注意:不停止 Web 服務器,保持持久性
|
|
313
|
+
debug_log("Web 服務器保持運行狀態")
|
|
314
|
+
|
|
315
|
+
# 注意:不清除桌面模式設置,保持 MCP_DESKTOP_MODE 環境變數
|
|
316
|
+
# 這樣下次 MCP 調用時仍然會啟動桌面應用程式
|
|
317
|
+
# self.set_desktop_mode(False) # 註釋掉這行
|
|
318
|
+
# [TAURI_CLOSE] 弹窗关闭
|
|
319
|
+
_tauri_logger.info(
|
|
320
|
+
f"project={(os.environ.get('MCP_PROJECT_DIR') or '(未设置)')} reason=stop",
|
|
321
|
+
"[TAURI_CLOSE] Tauri 桌面应用已停止",
|
|
322
|
+
)
|
|
323
|
+
debug_log("桌面應用程式已停止")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def launch_desktop_app(test_mode: bool = False) -> DesktopApp:
|
|
327
|
+
"""啟動桌面應用程式
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
test_mode: 是否為測試模式,測試模式下會創建測試會話
|
|
331
|
+
"""
|
|
332
|
+
debug_log("正在啟動桌面應用程式...")
|
|
333
|
+
|
|
334
|
+
app = DesktopApp()
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
# 啟動 Web 後端
|
|
338
|
+
server_url = await app.start_web_backend()
|
|
339
|
+
|
|
340
|
+
if test_mode:
|
|
341
|
+
# 測試模式:創建測試會話
|
|
342
|
+
debug_log("測試模式:創建測試會話")
|
|
343
|
+
app.create_test_session()
|
|
344
|
+
else:
|
|
345
|
+
# MCP 調用模式:使用現有會話
|
|
346
|
+
debug_log("MCP 調用模式:使用現有 MCP 會話,不創建新的測試會話")
|
|
347
|
+
|
|
348
|
+
# 啟動 Tauri 桌面應用程式
|
|
349
|
+
await app.launch_tauri_app(server_url)
|
|
350
|
+
|
|
351
|
+
debug_log(f"桌面應用程式已啟動,後端服務: {server_url}")
|
|
352
|
+
return app
|
|
353
|
+
|
|
354
|
+
except Exception as e:
|
|
355
|
+
debug_log(f"桌面應用程式啟動失敗: {e}")
|
|
356
|
+
app.stop()
|
|
357
|
+
raise
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def run_desktop_app():
|
|
361
|
+
"""同步方式運行桌面應用程式"""
|
|
362
|
+
try:
|
|
363
|
+
# 設置事件循環策略(Windows)
|
|
364
|
+
if sys.platform == "win32":
|
|
365
|
+
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
366
|
+
|
|
367
|
+
# 運行應用程式
|
|
368
|
+
loop = asyncio.new_event_loop()
|
|
369
|
+
asyncio.set_event_loop(loop)
|
|
370
|
+
|
|
371
|
+
app = loop.run_until_complete(launch_desktop_app())
|
|
372
|
+
|
|
373
|
+
# 保持應用程式運行
|
|
374
|
+
debug_log("桌面應用程式正在運行,按 Ctrl+C 停止...")
|
|
375
|
+
try:
|
|
376
|
+
while True:
|
|
377
|
+
time.sleep(1)
|
|
378
|
+
except KeyboardInterrupt:
|
|
379
|
+
debug_log("收到停止信號...")
|
|
380
|
+
finally:
|
|
381
|
+
app.stop()
|
|
382
|
+
loop.close()
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
sys.stderr.write(f"桌面應用程式運行失敗: {e}\n")
|
|
386
|
+
sys.exit(1)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
if __name__ == "__main__":
|
|
390
|
+
run_desktop_app()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""反馈处理辅助函数
|
|
2
|
+
|
|
3
|
+
从 server.py 提取,供 server.py 和 delegate_manager.py 共同使用,
|
|
4
|
+
避免 server → web.main → delegate_manager → server 的循环 import。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from ..debug import server_debug_log as debug_log
|
|
12
|
+
from ..utils.resource_manager import create_temp_file
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def save_feedback_to_file(feedback_data: dict, file_path: str | None = None) -> str:
|
|
16
|
+
"""
|
|
17
|
+
將回饋資料儲存到 JSON 文件
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
feedback_data: 回饋資料字典
|
|
21
|
+
file_path: 儲存路徑,若為 None 則自動產生臨時文件
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
str: 儲存的文件路徑
|
|
25
|
+
"""
|
|
26
|
+
if file_path is None:
|
|
27
|
+
file_path = create_temp_file(suffix=".json", prefix="feedback_")
|
|
28
|
+
|
|
29
|
+
directory = os.path.dirname(file_path)
|
|
30
|
+
if directory and not os.path.exists(directory):
|
|
31
|
+
os.makedirs(directory, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
json_data = feedback_data.copy()
|
|
34
|
+
|
|
35
|
+
if "images" in json_data and isinstance(json_data["images"], list):
|
|
36
|
+
processed_images = []
|
|
37
|
+
for img in json_data["images"]:
|
|
38
|
+
if isinstance(img, dict) and "data" in img:
|
|
39
|
+
processed_img = img.copy()
|
|
40
|
+
if isinstance(img["data"], bytes):
|
|
41
|
+
processed_img["data"] = base64.b64encode(img["data"]).decode(
|
|
42
|
+
"utf-8"
|
|
43
|
+
)
|
|
44
|
+
processed_img["data_type"] = "base64"
|
|
45
|
+
processed_images.append(processed_img)
|
|
46
|
+
else:
|
|
47
|
+
processed_images.append(img)
|
|
48
|
+
json_data["images"] = processed_images
|
|
49
|
+
|
|
50
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
51
|
+
json.dump(json_data, f, ensure_ascii=False, indent=2)
|
|
52
|
+
|
|
53
|
+
debug_log(f"反馈数据已保存至: {file_path}")
|
|
54
|
+
return file_path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_feedback_text(feedback_data: dict) -> str:
|
|
58
|
+
"""
|
|
59
|
+
建立格式化的回饋文字
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
feedback_data: 回饋資料字典
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: 格式化後的回饋文字
|
|
66
|
+
"""
|
|
67
|
+
text_parts = []
|
|
68
|
+
|
|
69
|
+
if feedback_data.get("interactive_feedback"):
|
|
70
|
+
text_parts.append(feedback_data["interactive_feedback"])
|
|
71
|
+
|
|
72
|
+
if feedback_data.get("command_logs"):
|
|
73
|
+
text_parts.append(f"=== 命令執行日誌 ===\n{feedback_data['command_logs']}")
|
|
74
|
+
|
|
75
|
+
if feedback_data.get("images"):
|
|
76
|
+
images = feedback_data["images"]
|
|
77
|
+
text_parts.append(f"=== 圖片附件概要 ===\n用戶提供了 {len(images)} 張圖片:")
|
|
78
|
+
|
|
79
|
+
for i, img in enumerate(images, 1):
|
|
80
|
+
size = img.get("size", 0)
|
|
81
|
+
name = img.get("name", "unknown")
|
|
82
|
+
|
|
83
|
+
if size < 1024:
|
|
84
|
+
size_str = f"{size} B"
|
|
85
|
+
elif size < 1024 * 1024:
|
|
86
|
+
size_kb = size / 1024
|
|
87
|
+
size_str = f"{size_kb:.1f} KB"
|
|
88
|
+
else:
|
|
89
|
+
size_mb = size / (1024 * 1024)
|
|
90
|
+
size_str = f"{size_mb:.1f} MB"
|
|
91
|
+
|
|
92
|
+
img_info = f" {i}. {name} ({size_str})"
|
|
93
|
+
|
|
94
|
+
if img.get("data"):
|
|
95
|
+
try:
|
|
96
|
+
if isinstance(img["data"], bytes):
|
|
97
|
+
img_base64 = base64.b64encode(img["data"]).decode("utf-8")
|
|
98
|
+
elif isinstance(img["data"], str):
|
|
99
|
+
img_base64 = img["data"]
|
|
100
|
+
else:
|
|
101
|
+
img_base64 = None
|
|
102
|
+
|
|
103
|
+
if img_base64:
|
|
104
|
+
preview = (
|
|
105
|
+
img_base64[:50] + "..."
|
|
106
|
+
if len(img_base64) > 50
|
|
107
|
+
else img_base64
|
|
108
|
+
)
|
|
109
|
+
img_info += f"\n Base64 預覽: {preview}"
|
|
110
|
+
img_info += f"\n 完整 Base64 長度: {len(img_base64)} 字符"
|
|
111
|
+
|
|
112
|
+
debug_log(f"图片 {i} Base64 已准备,长度: {len(img_base64)}")
|
|
113
|
+
|
|
114
|
+
include_full_base64 = feedback_data.get("settings", {}).get(
|
|
115
|
+
"enable_base64_detail", False
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if include_full_base64:
|
|
119
|
+
file_name = img.get("name", "image.png")
|
|
120
|
+
if file_name.lower().endswith((".jpg", ".jpeg")):
|
|
121
|
+
mime_type = "image/jpeg"
|
|
122
|
+
elif file_name.lower().endswith(".gif"):
|
|
123
|
+
mime_type = "image/gif"
|
|
124
|
+
elif file_name.lower().endswith(".webp"):
|
|
125
|
+
mime_type = "image/webp"
|
|
126
|
+
else:
|
|
127
|
+
mime_type = "image/png"
|
|
128
|
+
|
|
129
|
+
img_info += f"\n 完整 Base64: data:{mime_type};base64,{img_base64}"
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
debug_log(f"图片 {i} Base64 处理失败: {e}")
|
|
133
|
+
|
|
134
|
+
text_parts.append(img_info)
|
|
135
|
+
|
|
136
|
+
text_parts.append(
|
|
137
|
+
"\n💡 注意:如果 AI 助手無法顯示圖片,圖片數據已包含在上述 Base64 信息中。"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return "\n\n".join(text_parts) if text_parts else "用戶未提供任何回饋內容。"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""图片处理辅助函数
|
|
2
|
+
|
|
3
|
+
从 server.py 提取,供 server.py 和 delegate_manager.py 共同使用,
|
|
4
|
+
避免 server → web.main → delegate_manager → server 的循环 import。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
|
|
9
|
+
from mcp.types import ImageContent
|
|
10
|
+
|
|
11
|
+
from ..debug import server_debug_log as debug_log
|
|
12
|
+
from ..utils.error_handler import ErrorHandler, ErrorType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def process_images(images_data: list[dict]) -> list[ImageContent]:
|
|
16
|
+
"""
|
|
17
|
+
處理圖片資料,轉換為 MCP ImageContent 對象
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
images_data: 圖片資料列表
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List[ImageContent]: MCP 圖片內容對象列表
|
|
24
|
+
"""
|
|
25
|
+
mcp_images = []
|
|
26
|
+
|
|
27
|
+
for i, img in enumerate(images_data, 1):
|
|
28
|
+
try:
|
|
29
|
+
if not img.get("data"):
|
|
30
|
+
debug_log(f"图片 {i} 没有数据,跳过")
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if isinstance(img["data"], bytes):
|
|
34
|
+
base64_data = base64.b64encode(img["data"]).decode("utf-8")
|
|
35
|
+
debug_log(
|
|
36
|
+
f"图片 {i} 从原始 bytes 编码为 base64,原始大小: {len(img['data'])} bytes"
|
|
37
|
+
)
|
|
38
|
+
elif isinstance(img["data"], str):
|
|
39
|
+
base64_data = img["data"]
|
|
40
|
+
debug_log(f"图片 {i} 使用现有 base64 数据")
|
|
41
|
+
else:
|
|
42
|
+
debug_log(f"图片 {i} 数据类型不支持: {type(img['data'])}")
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if len(base64_data) == 0:
|
|
46
|
+
debug_log(f"图片 {i} 数据为空,跳过")
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
file_name = img.get("name", "image.png")
|
|
50
|
+
if file_name.lower().endswith((".jpg", ".jpeg")):
|
|
51
|
+
mime_type = "image/jpeg"
|
|
52
|
+
elif file_name.lower().endswith(".gif"):
|
|
53
|
+
mime_type = "image/gif"
|
|
54
|
+
elif file_name.lower().endswith(".webp"):
|
|
55
|
+
mime_type = "image/webp"
|
|
56
|
+
else:
|
|
57
|
+
mime_type = "image/png"
|
|
58
|
+
|
|
59
|
+
mcp_image = ImageContent(type="image", data=base64_data, mimeType=mime_type)
|
|
60
|
+
mcp_images.append(mcp_image)
|
|
61
|
+
|
|
62
|
+
debug_log(f"图片 {i} ({file_name}) 处理成功,格式: {mime_type}")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
error_id = ErrorHandler.log_error_with_context(
|
|
66
|
+
e,
|
|
67
|
+
context={"operation": "圖片處理", "image_index": i},
|
|
68
|
+
error_type=ErrorType.FILE_IO,
|
|
69
|
+
)
|
|
70
|
+
debug_log(f"图片 {i} 处理失败 [错误ID: {error_id}]: {e}")
|
|
71
|
+
|
|
72
|
+
debug_log(f"共处理 {len(mcp_images)} 张图片")
|
|
73
|
+
return mcp_images
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""代理模块:重定向到 mcp_supervisor_core.i18n
|
|
2
|
+
|
|
3
|
+
重构过渡期保持向后兼容。
|
|
4
|
+
Phase 3 删除 src/ 时此文件一并删除。
|
|
5
|
+
"""
|
|
6
|
+
from mcp_supervisor_core.i18n import * # noqa: F401,F403
|
|
7
|
+
from mcp_supervisor_core.i18n import ( # noqa: F401
|
|
8
|
+
I18nManager,
|
|
9
|
+
get_current_language,
|
|
10
|
+
get_i18n_manager,
|
|
11
|
+
reload_translations,
|
|
12
|
+
set_language,
|
|
13
|
+
t,
|
|
14
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""代理模块:重定向到 mcp_supervisor_core.log_writer
|
|
2
|
+
|
|
3
|
+
重构过渡期保持向后兼容。
|
|
4
|
+
Phase 3 删除 src/ 时此文件一并删除。
|
|
5
|
+
"""
|
|
6
|
+
from mcp_supervisor_core.log_writer import * # noqa: F401,F403
|
|
7
|
+
from mcp_supervisor_core.log_writer import ( # noqa: F401
|
|
8
|
+
SupervisorLogger,
|
|
9
|
+
_cleanup_old_logs,
|
|
10
|
+
_normalize_path,
|
|
11
|
+
_project_hash,
|
|
12
|
+
_update_projects_json,
|
|
13
|
+
get_logger,
|
|
14
|
+
redirect_stdlib_logging,
|
|
15
|
+
reset_logger_cache,
|
|
16
|
+
)
|