feedback-mcp 1.0.64__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.
Potentially problematic release.
This version of feedback-mcp might be problematic. Click here for more details.
- add_command_dialog.py +712 -0
- command.py +636 -0
- components/__init__.py +15 -0
- components/agent_popup.py +187 -0
- components/chat_history.py +281 -0
- components/command_popup.py +399 -0
- components/feedback_text_edit.py +1125 -0
- components/file_popup.py +417 -0
- components/history_popup.py +582 -0
- components/markdown_display.py +262 -0
- context_formatter.py +301 -0
- debug_logger.py +107 -0
- feedback_config.py +144 -0
- feedback_mcp-1.0.64.dist-info/METADATA +327 -0
- feedback_mcp-1.0.64.dist-info/RECORD +41 -0
- feedback_mcp-1.0.64.dist-info/WHEEL +5 -0
- feedback_mcp-1.0.64.dist-info/entry_points.txt +2 -0
- feedback_mcp-1.0.64.dist-info/top_level.txt +20 -0
- feedback_ui.py +1680 -0
- get_session_id.py +53 -0
- git_operations.py +579 -0
- ide_utils.py +313 -0
- path_config.py +89 -0
- post_task_hook.py +78 -0
- record.py +188 -0
- server.py +746 -0
- session_manager.py +368 -0
- stop_hook.py +87 -0
- tabs/__init__.py +87 -0
- tabs/base_tab.py +34 -0
- tabs/chat_history_style.qss +66 -0
- tabs/chat_history_tab.py +1000 -0
- tabs/chat_tab.py +1931 -0
- tabs/workspace_tab.py +502 -0
- ui/__init__.py +20 -0
- ui/__main__.py +16 -0
- ui/compact_feedback_ui.py +376 -0
- ui/session_list_ui.py +793 -0
- ui/styles/session_list.qss +158 -0
- window_position_manager.py +197 -0
- workspace_manager.py +253 -0
ui/session_list_ui.py
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
"""
|
|
2
|
+
会话列表UI - 显示所有等待回复的会话
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import socket
|
|
7
|
+
import json
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import pickle
|
|
13
|
+
from typing import Optional, List, Dict
|
|
14
|
+
from PySide6.QtWidgets import (
|
|
15
|
+
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QProgressBar, QPushButton
|
|
16
|
+
)
|
|
17
|
+
from PySide6.QtCore import Qt, QTimer
|
|
18
|
+
from PySide6.QtGui import QGuiApplication
|
|
19
|
+
|
|
20
|
+
# 添加路径以导入session_manager
|
|
21
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
22
|
+
try:
|
|
23
|
+
from session_manager import SessionManager
|
|
24
|
+
except ImportError:
|
|
25
|
+
SessionManager = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SessionListUI(QMainWindow):
|
|
29
|
+
"""会话列表UI - 单例模式"""
|
|
30
|
+
|
|
31
|
+
_instance: Optional['SessionListUI'] = None
|
|
32
|
+
SOCKET_HOST = "127.0.0.1"
|
|
33
|
+
SOCKET_PORT = 19876
|
|
34
|
+
|
|
35
|
+
def __new__(cls):
|
|
36
|
+
if cls._instance is None:
|
|
37
|
+
cls._instance = super().__new__(cls)
|
|
38
|
+
return cls._instance
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
if hasattr(self, '_initialized'):
|
|
42
|
+
return
|
|
43
|
+
super().__init__()
|
|
44
|
+
self._initialized = True
|
|
45
|
+
|
|
46
|
+
# 用于拖动窗口
|
|
47
|
+
self.dragging = False
|
|
48
|
+
self.drag_start_pos = None
|
|
49
|
+
self.mouse_press_time = 0
|
|
50
|
+
self.mouse_press_pos = None
|
|
51
|
+
|
|
52
|
+
# 折叠状态
|
|
53
|
+
self.is_collapsed = False
|
|
54
|
+
self.expanded_height = 400
|
|
55
|
+
|
|
56
|
+
# 呼吸动画状态
|
|
57
|
+
self.glow_phase = 0
|
|
58
|
+
self.has_new_feedback = False
|
|
59
|
+
|
|
60
|
+
# 会话数据管理
|
|
61
|
+
self.sessions: Dict[str, Dict] = {} # request_id -> session_data
|
|
62
|
+
self.session_sockets: Dict[str, socket.socket] = {} # request_id -> socket
|
|
63
|
+
self.feedback_processes: Dict[str, subprocess.Popen] = {} # request_id -> process
|
|
64
|
+
self.sessions_lock = threading.Lock()
|
|
65
|
+
|
|
66
|
+
# 设置窗口属性
|
|
67
|
+
self.setWindowTitle("等待回复")
|
|
68
|
+
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
|
|
69
|
+
self.setAttribute(Qt.WA_TranslucentBackground)
|
|
70
|
+
self.setFixedSize(280, 400)
|
|
71
|
+
self.setWindowOpacity(0.95)
|
|
72
|
+
|
|
73
|
+
# 设置窗口位置
|
|
74
|
+
self._set_position()
|
|
75
|
+
|
|
76
|
+
# 创建UI
|
|
77
|
+
self._create_ui()
|
|
78
|
+
|
|
79
|
+
# 启动定时器更新会话列表
|
|
80
|
+
self.update_timer = QTimer()
|
|
81
|
+
self.update_timer.timeout.connect(self._update_sessions)
|
|
82
|
+
self.update_timer.start(1000) # 每秒更新
|
|
83
|
+
|
|
84
|
+
# 启动Socket服务器
|
|
85
|
+
self.socket_thread = threading.Thread(target=self._run_socket_server, daemon=True)
|
|
86
|
+
self.socket_thread.start()
|
|
87
|
+
|
|
88
|
+
def _create_ui(self):
|
|
89
|
+
"""创建UI布局"""
|
|
90
|
+
# 加载QSS样式表
|
|
91
|
+
qss_path = os.path.join(os.path.dirname(__file__), 'styles', 'session_list.qss')
|
|
92
|
+
if os.path.exists(qss_path):
|
|
93
|
+
with open(qss_path, 'r', encoding='utf-8') as f:
|
|
94
|
+
self.setStyleSheet(f.read())
|
|
95
|
+
|
|
96
|
+
central_widget = QWidget()
|
|
97
|
+
central_widget.setObjectName("mainContainer")
|
|
98
|
+
central_widget.setStyleSheet("background-color: rgba(35, 35, 35, 240); border-radius: 16px;")
|
|
99
|
+
self.setCentralWidget(central_widget)
|
|
100
|
+
|
|
101
|
+
layout = QVBoxLayout(central_widget)
|
|
102
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
103
|
+
layout.setSpacing(0)
|
|
104
|
+
|
|
105
|
+
# 标题栏
|
|
106
|
+
title_bar = self._create_title_bar()
|
|
107
|
+
layout.addWidget(title_bar)
|
|
108
|
+
|
|
109
|
+
# 会话列表区域
|
|
110
|
+
self.scroll_area = QScrollArea()
|
|
111
|
+
self.scroll_area.setObjectName("scrollArea")
|
|
112
|
+
self.scroll_area.setWidgetResizable(True)
|
|
113
|
+
|
|
114
|
+
self.session_container = QWidget()
|
|
115
|
+
self.session_container.setObjectName("sessionContainer")
|
|
116
|
+
self.session_layout = QVBoxLayout(self.session_container)
|
|
117
|
+
self.session_layout.setContentsMargins(10, 10, 10, 10)
|
|
118
|
+
self.session_layout.setSpacing(8)
|
|
119
|
+
self.session_layout.addStretch()
|
|
120
|
+
|
|
121
|
+
self.scroll_area.setWidget(self.session_container)
|
|
122
|
+
layout.addWidget(self.scroll_area)
|
|
123
|
+
|
|
124
|
+
def _create_title_bar(self) -> QWidget:
|
|
125
|
+
"""创建标题栏"""
|
|
126
|
+
title_bar = QWidget()
|
|
127
|
+
title_bar.setObjectName("titleBar")
|
|
128
|
+
title_bar.setFixedHeight(40)
|
|
129
|
+
title_bar.setStyleSheet("background-color: rgba(60, 60, 60, 255); border-top-left-radius: 15px; border-top-right-radius: 15px;")
|
|
130
|
+
title_bar.setCursor(Qt.PointingHandCursor)
|
|
131
|
+
# 保存title_bar引用以便在鼠标事件中识别
|
|
132
|
+
self.title_bar = title_bar
|
|
133
|
+
|
|
134
|
+
layout = QHBoxLayout(title_bar)
|
|
135
|
+
layout.setContentsMargins(15, 0, 15, 0)
|
|
136
|
+
|
|
137
|
+
self.title_label = QLabel("📋 等待回复 (0)")
|
|
138
|
+
self.title_label.setObjectName("titleLabel")
|
|
139
|
+
self.title_label.setStyleSheet("color: white; font-size: 13px; font-weight: bold; background-color: transparent;")
|
|
140
|
+
layout.addWidget(self.title_label, alignment=Qt.AlignVCenter)
|
|
141
|
+
|
|
142
|
+
layout.addStretch()
|
|
143
|
+
|
|
144
|
+
self.collapse_btn = QPushButton("▼")
|
|
145
|
+
self.collapse_btn.setObjectName("collapseButton")
|
|
146
|
+
self.collapse_btn.setFixedSize(24, 24)
|
|
147
|
+
self.collapse_btn.setStyleSheet("background-color: transparent; color: rgba(255, 255, 255, 180); border: none; font-size: 12px;")
|
|
148
|
+
self.collapse_btn.clicked.connect(self._toggle_collapse)
|
|
149
|
+
layout.addWidget(self.collapse_btn, alignment=Qt.AlignVCenter)
|
|
150
|
+
|
|
151
|
+
# 呼吸动画定时器
|
|
152
|
+
self.glow_timer = QTimer()
|
|
153
|
+
self.glow_timer.timeout.connect(self._update_glow_effect)
|
|
154
|
+
|
|
155
|
+
return title_bar
|
|
156
|
+
|
|
157
|
+
def _toggle_collapse(self):
|
|
158
|
+
"""切换折叠/展开状态"""
|
|
159
|
+
self.is_collapsed = not self.is_collapsed
|
|
160
|
+
|
|
161
|
+
if self.is_collapsed:
|
|
162
|
+
self.collapse_btn.setText("▲")
|
|
163
|
+
self.scroll_area.hide()
|
|
164
|
+
self.setFixedHeight(40)
|
|
165
|
+
self.setWindowOpacity(0.5)
|
|
166
|
+
else:
|
|
167
|
+
self.collapse_btn.setText("▼")
|
|
168
|
+
self.scroll_area.show()
|
|
169
|
+
self.setFixedHeight(self.expanded_height)
|
|
170
|
+
self.setWindowOpacity(1.0)
|
|
171
|
+
|
|
172
|
+
def _update_glow_effect(self):
|
|
173
|
+
"""更新呼吸发光效果"""
|
|
174
|
+
self.glow_phase = (self.glow_phase + 2) % 100
|
|
175
|
+
|
|
176
|
+
import math
|
|
177
|
+
alpha = int(255 * abs(math.sin(self.glow_phase * math.pi / 100)))
|
|
178
|
+
|
|
179
|
+
self.title_bar.setStyleSheet(f"""
|
|
180
|
+
QWidget#titleBar {{
|
|
181
|
+
background-color: rgba(60, 60, 60, 255);
|
|
182
|
+
border-top-left-radius: 15px;
|
|
183
|
+
border-top-right-radius: 15px;
|
|
184
|
+
border: 3px solid rgba(0, 200, 80, {alpha});
|
|
185
|
+
}}
|
|
186
|
+
QLabel#titleLabel {{
|
|
187
|
+
color: white;
|
|
188
|
+
font-size: 13px;
|
|
189
|
+
font-weight: bold;
|
|
190
|
+
background-color: transparent;
|
|
191
|
+
border: none;
|
|
192
|
+
}}
|
|
193
|
+
QPushButton#collapseButton {{
|
|
194
|
+
background-color: transparent;
|
|
195
|
+
color: rgba(255, 255, 255, 180);
|
|
196
|
+
border: none;
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
}}
|
|
199
|
+
""")
|
|
200
|
+
|
|
201
|
+
def _create_session_item(self, session: Dict) -> QWidget:
|
|
202
|
+
"""创建会话项"""
|
|
203
|
+
item = QWidget()
|
|
204
|
+
item.setObjectName("sessionCard")
|
|
205
|
+
item.setAttribute(Qt.WA_Hover, True) # 启用hover事件
|
|
206
|
+
item.setCursor(Qt.PointingHandCursor) # 鼠标指针变为手型
|
|
207
|
+
is_new = session.get('is_new', False)
|
|
208
|
+
border_color = "#4CAF50" if is_new else "rgba(255, 255, 255, 10)"
|
|
209
|
+
hover_border = "#66BB6A" if is_new else "rgba(255, 255, 255, 25)"
|
|
210
|
+
# 使用精确选择器,包含hover效果
|
|
211
|
+
item.setStyleSheet(f"""
|
|
212
|
+
QWidget#sessionCard {{
|
|
213
|
+
background-color: rgba(60, 60, 60, 200);
|
|
214
|
+
border-radius: 8px;
|
|
215
|
+
border: 1px solid {border_color};
|
|
216
|
+
}}
|
|
217
|
+
QWidget#sessionCard:hover {{
|
|
218
|
+
background-color: rgba(75, 75, 75, 230);
|
|
219
|
+
border: 1px solid {hover_border};
|
|
220
|
+
}}
|
|
221
|
+
""")
|
|
222
|
+
|
|
223
|
+
# 设置鼠标点击事件,使用request_id而非session副本
|
|
224
|
+
request_id = session.get('request_id')
|
|
225
|
+
item.mousePressEvent = lambda event, rid=request_id: self._on_session_clicked(rid)
|
|
226
|
+
|
|
227
|
+
layout = QVBoxLayout(item)
|
|
228
|
+
layout.setContentsMargins(8, 6, 8, 6)
|
|
229
|
+
layout.setSpacing(2)
|
|
230
|
+
|
|
231
|
+
# 第一行:项目名称 + 关闭按钮
|
|
232
|
+
header_widget = QWidget()
|
|
233
|
+
header_widget.setStyleSheet("background-color: transparent;")
|
|
234
|
+
header_layout = QHBoxLayout(header_widget)
|
|
235
|
+
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
236
|
+
header_layout.setSpacing(4)
|
|
237
|
+
|
|
238
|
+
# 项目名称
|
|
239
|
+
project_path = session.get('project_path', '')
|
|
240
|
+
project_name = os.path.basename(project_path) if project_path else '未知'
|
|
241
|
+
if len(project_name) > 25:
|
|
242
|
+
project_name = project_name[:23] + ".."
|
|
243
|
+
project_label = QLabel(f"📁 项目: {project_name}")
|
|
244
|
+
project_label.setStyleSheet("color: #FF9800; font-size: 11px; background-color: transparent;")
|
|
245
|
+
header_layout.addWidget(project_label)
|
|
246
|
+
|
|
247
|
+
header_layout.addStretch()
|
|
248
|
+
|
|
249
|
+
# 关闭按钮
|
|
250
|
+
close_btn = QPushButton("×")
|
|
251
|
+
close_btn.setFixedSize(18, 18)
|
|
252
|
+
close_btn.setCursor(Qt.PointingHandCursor)
|
|
253
|
+
close_btn.setStyleSheet("""
|
|
254
|
+
QPushButton {
|
|
255
|
+
background-color: transparent;
|
|
256
|
+
color: #888;
|
|
257
|
+
border: none;
|
|
258
|
+
font-size: 14px;
|
|
259
|
+
font-weight: bold;
|
|
260
|
+
}
|
|
261
|
+
QPushButton:hover {
|
|
262
|
+
color: #ff5555;
|
|
263
|
+
}
|
|
264
|
+
""")
|
|
265
|
+
close_btn.clicked.connect(lambda checked, rid=request_id: self._on_close_clicked(rid))
|
|
266
|
+
header_layout.addWidget(close_btn)
|
|
267
|
+
|
|
268
|
+
layout.addWidget(header_widget)
|
|
269
|
+
|
|
270
|
+
# 工作空间名称
|
|
271
|
+
workspace = session.get('workspace_id') or session.get('work_title', '未知')
|
|
272
|
+
workspace_label = QLabel(f"📦 工作空间: {workspace}")
|
|
273
|
+
workspace_label.setStyleSheet("color: #4CAF50; font-size: 11px; background-color: transparent;")
|
|
274
|
+
workspace_label.setMaximumWidth(260)
|
|
275
|
+
font_metrics = workspace_label.fontMetrics()
|
|
276
|
+
elided_text = font_metrics.elidedText(f"📦 工作空间: {workspace}", Qt.ElideRight, 255)
|
|
277
|
+
workspace_label.setText(elided_text)
|
|
278
|
+
layout.addWidget(workspace_label)
|
|
279
|
+
|
|
280
|
+
# 阶段信息
|
|
281
|
+
stage = session.get('stage', '未知')
|
|
282
|
+
stage_label = QLabel(f"📍 阶段: {stage}")
|
|
283
|
+
stage_label.setStyleSheet("color: #64B5F6; font-size: 11px; background-color: transparent;")
|
|
284
|
+
layout.addWidget(stage_label)
|
|
285
|
+
|
|
286
|
+
# 对话标题
|
|
287
|
+
conversation = session.get('session_title') or session.get('work_title', '无标题')
|
|
288
|
+
conversation_label = QLabel(f"💬 对话: {conversation}")
|
|
289
|
+
conversation_label.setStyleSheet("color: white; font-size: 11px; background-color: transparent;")
|
|
290
|
+
conversation_label.setMaximumWidth(260)
|
|
291
|
+
font_metrics = conversation_label.fontMetrics()
|
|
292
|
+
elided_text = font_metrics.elidedText(f"💬 对话: {conversation}", Qt.ElideRight, 255)
|
|
293
|
+
conversation_label.setText(elided_text)
|
|
294
|
+
layout.addWidget(conversation_label)
|
|
295
|
+
|
|
296
|
+
# 进度条和计时
|
|
297
|
+
elapsed = session.get('elapsed_time', 0)
|
|
298
|
+
timeout = session.get('timeout', 3600)
|
|
299
|
+
progress = min(int((elapsed / timeout) * 100), 100)
|
|
300
|
+
|
|
301
|
+
progress_container = QWidget()
|
|
302
|
+
progress_container.setStyleSheet("background-color: transparent;")
|
|
303
|
+
progress_layout = QHBoxLayout(progress_container)
|
|
304
|
+
progress_layout.setContentsMargins(0, 2, 0, 0)
|
|
305
|
+
progress_layout.setSpacing(8)
|
|
306
|
+
|
|
307
|
+
progress_bar = QProgressBar()
|
|
308
|
+
progress_bar.setMaximum(100)
|
|
309
|
+
progress_bar.setValue(progress)
|
|
310
|
+
progress_bar.setTextVisible(False)
|
|
311
|
+
progress_bar.setFixedHeight(4)
|
|
312
|
+
progress_bar.setStyleSheet("""
|
|
313
|
+
QProgressBar { background-color: rgba(255, 255, 255, 10); border: none; border-radius: 2px; }
|
|
314
|
+
QProgressBar::chunk { background-color: #4CAF50; border-radius: 2px; }
|
|
315
|
+
""")
|
|
316
|
+
progress_layout.addWidget(progress_bar)
|
|
317
|
+
|
|
318
|
+
time_label = QLabel(f"{elapsed // 60}:{elapsed % 60:02d}")
|
|
319
|
+
time_label.setStyleSheet("color: rgba(255, 255, 255, 180); font-size: 10px; background-color: transparent;")
|
|
320
|
+
time_label.setFixedWidth(35)
|
|
321
|
+
progress_layout.addWidget(time_label)
|
|
322
|
+
|
|
323
|
+
layout.addWidget(progress_container)
|
|
324
|
+
|
|
325
|
+
return item
|
|
326
|
+
|
|
327
|
+
def _format_time(self, seconds: int) -> str:
|
|
328
|
+
"""格式化时间显示"""
|
|
329
|
+
if seconds < 60:
|
|
330
|
+
return f"{seconds}秒"
|
|
331
|
+
else:
|
|
332
|
+
minutes = seconds // 60
|
|
333
|
+
secs = seconds % 60
|
|
334
|
+
return f"{minutes}分{secs}秒"
|
|
335
|
+
|
|
336
|
+
def _on_session_clicked(self, request_id: str):
|
|
337
|
+
"""处理会话项点击事件"""
|
|
338
|
+
# 检查是否已在处理中,防止重复点击
|
|
339
|
+
with self.sessions_lock:
|
|
340
|
+
if request_id not in self.sessions:
|
|
341
|
+
print(f"会话 {request_id} 不存在")
|
|
342
|
+
return
|
|
343
|
+
if self.sessions[request_id].get('is_processing'):
|
|
344
|
+
print(f"会话 {request_id} 已在处理中,忽略重复点击")
|
|
345
|
+
return
|
|
346
|
+
self.sessions[request_id]['is_processing'] = True
|
|
347
|
+
self.sessions[request_id]['is_new'] = False
|
|
348
|
+
# 获取会��数据的副本
|
|
349
|
+
session = self.sessions[request_id].copy()
|
|
350
|
+
|
|
351
|
+
print(f"点击会话: {request_id}")
|
|
352
|
+
|
|
353
|
+
# 在新线程中启动FeedbackUI
|
|
354
|
+
threading.Thread(
|
|
355
|
+
target=self._launch_feedback_ui,
|
|
356
|
+
args=(session,),
|
|
357
|
+
daemon=True
|
|
358
|
+
).start()
|
|
359
|
+
|
|
360
|
+
def _on_close_clicked(self, request_id: str):
|
|
361
|
+
"""处理关闭按钮点击,发送STOP消息"""
|
|
362
|
+
with self.sessions_lock:
|
|
363
|
+
if request_id not in self.sessions:
|
|
364
|
+
return
|
|
365
|
+
session = self.sessions[request_id].copy()
|
|
366
|
+
|
|
367
|
+
# 关闭对应的FeedbackUI进程(如果存在)
|
|
368
|
+
if request_id in self.feedback_processes:
|
|
369
|
+
try:
|
|
370
|
+
process = self.feedback_processes[request_id]
|
|
371
|
+
process.terminate()
|
|
372
|
+
del self.feedback_processes[request_id]
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
# 记录用户主动关闭状态
|
|
377
|
+
session_id = session.get('session_id')
|
|
378
|
+
project_path = session.get('project_path')
|
|
379
|
+
if session_id and SessionManager:
|
|
380
|
+
try:
|
|
381
|
+
manager = SessionManager(session_id=session_id, project_path=project_path)
|
|
382
|
+
manager.mark_user_closed_by_button(session_id)
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
# 发送STOP消息
|
|
387
|
+
result = {
|
|
388
|
+
'interactive_feedback': 'STOP!请立即停止任何工作,不要再调用任何工具、回复任何消息。STOP!',
|
|
389
|
+
'images': []
|
|
390
|
+
}
|
|
391
|
+
self.send_response(request_id, result)
|
|
392
|
+
|
|
393
|
+
def _launch_feedback_ui(self, session: Dict):
|
|
394
|
+
"""启动FeedbackUI子进程"""
|
|
395
|
+
try:
|
|
396
|
+
request_id = session.get('request_id')
|
|
397
|
+
|
|
398
|
+
# 创建临时文件接收结果
|
|
399
|
+
with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.pkl') as f:
|
|
400
|
+
output_file = f.name
|
|
401
|
+
|
|
402
|
+
# 构建FeedbackUI启动命令
|
|
403
|
+
feedback_script = os.path.join(
|
|
404
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
405
|
+
'feedback_ui.py'
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# 构建命令行参数
|
|
409
|
+
cmd = [
|
|
410
|
+
sys.executable,
|
|
411
|
+
feedback_script,
|
|
412
|
+
'--prompt', session.get('message', ''),
|
|
413
|
+
'--project-path', session.get('project_path', ''),
|
|
414
|
+
'--work-title', session.get('work_title', ''),
|
|
415
|
+
'--timeout', str(session.get('timeout', 3600)),
|
|
416
|
+
'--output-file', output_file,
|
|
417
|
+
'--skip-init-check'
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
# 添加可选参数
|
|
421
|
+
if session.get('session_id'):
|
|
422
|
+
cmd.extend(['--session-id', session.get('session_id')])
|
|
423
|
+
|
|
424
|
+
if session.get('predefined_options'):
|
|
425
|
+
options_str = '|||'.join(session.get('predefined_options'))
|
|
426
|
+
cmd.extend(['--predefined-options', options_str])
|
|
427
|
+
|
|
428
|
+
if session.get('files'):
|
|
429
|
+
files_str = '|||'.join(session.get('files'))
|
|
430
|
+
cmd.extend(['--files', files_str])
|
|
431
|
+
|
|
432
|
+
if session.get('workspace_id'):
|
|
433
|
+
cmd.extend(['--workspace-id', session.get('workspace_id')])
|
|
434
|
+
|
|
435
|
+
print(f"启动FeedbackUI: {' '.join(cmd)}")
|
|
436
|
+
|
|
437
|
+
# 启动子进程
|
|
438
|
+
process = subprocess.Popen(
|
|
439
|
+
cmd,
|
|
440
|
+
stdout=subprocess.PIPE,
|
|
441
|
+
stderr=subprocess.PIPE
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# 保存进程引用,以便可以从外部终止
|
|
445
|
+
with self.sessions_lock:
|
|
446
|
+
self.feedback_processes[request_id] = process
|
|
447
|
+
|
|
448
|
+
# 等待子进程完成
|
|
449
|
+
process.wait()
|
|
450
|
+
|
|
451
|
+
# 清理进程引用
|
|
452
|
+
with self.sessions_lock:
|
|
453
|
+
if request_id in self.feedback_processes:
|
|
454
|
+
del self.feedback_processes[request_id]
|
|
455
|
+
|
|
456
|
+
# 读取结果
|
|
457
|
+
result = None
|
|
458
|
+
if os.path.exists(output_file):
|
|
459
|
+
try:
|
|
460
|
+
with open(output_file, 'rb') as f:
|
|
461
|
+
result = pickle.load(f)
|
|
462
|
+
os.unlink(output_file)
|
|
463
|
+
except Exception as e:
|
|
464
|
+
print(f"读取结果文件失败: {e}")
|
|
465
|
+
|
|
466
|
+
# 发送结果给MCP服务器
|
|
467
|
+
if result:
|
|
468
|
+
self.send_response(request_id, result)
|
|
469
|
+
else:
|
|
470
|
+
# 用户关闭了窗口,不发送响应,保留会话项
|
|
471
|
+
print(f"用户关闭了FeedbackUI,保留会话: {request_id}")
|
|
472
|
+
# 重置处理状态,允许再次点击
|
|
473
|
+
with self.sessions_lock:
|
|
474
|
+
if request_id in self.sessions:
|
|
475
|
+
self.sessions[request_id]['is_processing'] = False
|
|
476
|
+
|
|
477
|
+
except Exception as e:
|
|
478
|
+
print(f"启动FeedbackUI失败: {e}")
|
|
479
|
+
import traceback
|
|
480
|
+
traceback.print_exc()
|
|
481
|
+
# 异常时也重置处理状态
|
|
482
|
+
with self.sessions_lock:
|
|
483
|
+
if request_id in self.sessions:
|
|
484
|
+
self.sessions[request_id]['is_processing'] = False
|
|
485
|
+
|
|
486
|
+
def _run_socket_server(self):
|
|
487
|
+
"""运行Socket服务器"""
|
|
488
|
+
# 创建TCP Socket
|
|
489
|
+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
490
|
+
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
491
|
+
server_socket.bind((self.SOCKET_HOST, self.SOCKET_PORT))
|
|
492
|
+
server_socket.listen(5)
|
|
493
|
+
print(f"Socket服务器启动: {self.SOCKET_HOST}:{self.SOCKET_PORT}")
|
|
494
|
+
|
|
495
|
+
while True:
|
|
496
|
+
try:
|
|
497
|
+
client_socket, _ = server_socket.accept()
|
|
498
|
+
# 为每个连接创建新线程处理
|
|
499
|
+
threading.Thread(
|
|
500
|
+
target=self._handle_client,
|
|
501
|
+
args=(client_socket,),
|
|
502
|
+
daemon=True
|
|
503
|
+
).start()
|
|
504
|
+
except Exception as e:
|
|
505
|
+
print(f"Socket服务器错误: {e}")
|
|
506
|
+
break
|
|
507
|
+
|
|
508
|
+
def _handle_client(self, client_socket: socket.socket):
|
|
509
|
+
"""处理客户端请求"""
|
|
510
|
+
try:
|
|
511
|
+
# 接收数据
|
|
512
|
+
data = b""
|
|
513
|
+
while True:
|
|
514
|
+
chunk = client_socket.recv(4096)
|
|
515
|
+
if not chunk:
|
|
516
|
+
break
|
|
517
|
+
data += chunk
|
|
518
|
+
# 简单判断:如果收到完整JSON(以}结尾),则停止接收
|
|
519
|
+
try:
|
|
520
|
+
json.loads(data.decode('utf-8'))
|
|
521
|
+
break
|
|
522
|
+
except:
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
if not data:
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# 解析请求
|
|
529
|
+
request = json.loads(data.decode('utf-8'))
|
|
530
|
+
action = request.get('action')
|
|
531
|
+
|
|
532
|
+
if action == 'add_session':
|
|
533
|
+
self._handle_add_session(request, client_socket)
|
|
534
|
+
else:
|
|
535
|
+
# 未知操作
|
|
536
|
+
response = {
|
|
537
|
+
"request_id": request.get('request_id'),
|
|
538
|
+
"status": "error",
|
|
539
|
+
"error": f"Unknown action: {action}"
|
|
540
|
+
}
|
|
541
|
+
client_socket.sendall(json.dumps(response).encode('utf-8'))
|
|
542
|
+
client_socket.close()
|
|
543
|
+
|
|
544
|
+
except Exception as e:
|
|
545
|
+
print(f"处理客户端请求失败: {e}")
|
|
546
|
+
try:
|
|
547
|
+
client_socket.close()
|
|
548
|
+
except:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
def _handle_add_session(self, request: Dict, client_socket: socket.socket):
|
|
552
|
+
"""处理添加会话请求"""
|
|
553
|
+
request_id = request.get('request_id')
|
|
554
|
+
|
|
555
|
+
with self.sessions_lock:
|
|
556
|
+
# 保存会话数据
|
|
557
|
+
self.sessions[request_id] = {
|
|
558
|
+
'request_id': request_id,
|
|
559
|
+
'session_id': request.get('session_id'),
|
|
560
|
+
'project_path': request.get('project_path'),
|
|
561
|
+
'work_title': request.get('work_title'),
|
|
562
|
+
'message': request.get('message'),
|
|
563
|
+
'predefined_options': request.get('predefined_options', []),
|
|
564
|
+
'files': request.get('files', []),
|
|
565
|
+
'timeout': request.get('timeout', 3600),
|
|
566
|
+
'start_time': time.time(),
|
|
567
|
+
'elapsed_time': 0,
|
|
568
|
+
'workspace_id': request.get('workspace_id'),
|
|
569
|
+
'stage': request.get('stage'),
|
|
570
|
+
'session_title': request.get('session_title'),
|
|
571
|
+
'is_new': True
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# 保存socket连接
|
|
575
|
+
self.session_sockets[request_id] = client_socket
|
|
576
|
+
|
|
577
|
+
print(f"添加会话: {request_id} - {request.get('work_title')}")
|
|
578
|
+
|
|
579
|
+
def add_session(self, request_id: str, session_data: Dict):
|
|
580
|
+
"""添加会话(供外部调用)"""
|
|
581
|
+
with self.sessions_lock:
|
|
582
|
+
self.sessions[request_id] = session_data
|
|
583
|
+
|
|
584
|
+
def remove_session(self, request_id: str):
|
|
585
|
+
"""移除会话"""
|
|
586
|
+
with self.sessions_lock:
|
|
587
|
+
if request_id in self.sessions:
|
|
588
|
+
del self.sessions[request_id]
|
|
589
|
+
if request_id in self.session_sockets:
|
|
590
|
+
try:
|
|
591
|
+
self.session_sockets[request_id].close()
|
|
592
|
+
except:
|
|
593
|
+
pass
|
|
594
|
+
del self.session_sockets[request_id]
|
|
595
|
+
|
|
596
|
+
def get_session(self, request_id: str) -> Optional[Dict]:
|
|
597
|
+
"""查询会话"""
|
|
598
|
+
with self.sessions_lock:
|
|
599
|
+
return self.sessions.get(request_id)
|
|
600
|
+
|
|
601
|
+
def send_response(self, request_id: str, result: Dict):
|
|
602
|
+
"""发送响应给MCP Server"""
|
|
603
|
+
with self.sessions_lock:
|
|
604
|
+
if request_id not in self.session_sockets:
|
|
605
|
+
print(f"未找到会话socket: {request_id}")
|
|
606
|
+
# 清理会话,确保从列表中移除
|
|
607
|
+
if request_id in self.sessions:
|
|
608
|
+
del self.sessions[request_id]
|
|
609
|
+
return False
|
|
610
|
+
|
|
611
|
+
client_socket = self.session_sockets[request_id]
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
response = {
|
|
615
|
+
"request_id": request_id,
|
|
616
|
+
"status": "success",
|
|
617
|
+
"result": result
|
|
618
|
+
}
|
|
619
|
+
client_socket.sendall(json.dumps(response).encode('utf-8'))
|
|
620
|
+
client_socket.close()
|
|
621
|
+
|
|
622
|
+
# 清理会话
|
|
623
|
+
del self.session_sockets[request_id]
|
|
624
|
+
if request_id in self.sessions:
|
|
625
|
+
del self.sessions[request_id]
|
|
626
|
+
|
|
627
|
+
return True
|
|
628
|
+
except Exception as e:
|
|
629
|
+
print(f"发送响应失败: {e}")
|
|
630
|
+
# 发送失败也要清理会话
|
|
631
|
+
if request_id in self.session_sockets:
|
|
632
|
+
del self.session_sockets[request_id]
|
|
633
|
+
if request_id in self.sessions:
|
|
634
|
+
del self.sessions[request_id]
|
|
635
|
+
return False
|
|
636
|
+
|
|
637
|
+
def _update_sessions(self):
|
|
638
|
+
"""更新会话列表"""
|
|
639
|
+
try:
|
|
640
|
+
with self.sessions_lock:
|
|
641
|
+
# 更新每个会话的等待时间
|
|
642
|
+
current_time = time.time()
|
|
643
|
+
for session in self.sessions.values():
|
|
644
|
+
session['elapsed_time'] = int(current_time - session['start_time'])
|
|
645
|
+
|
|
646
|
+
# 获取会话列表
|
|
647
|
+
sessions = list(self.sessions.values())
|
|
648
|
+
|
|
649
|
+
# 更新标题
|
|
650
|
+
count = len(sessions)
|
|
651
|
+
new_count = sum(1 for s in sessions if s.get('is_new', False))
|
|
652
|
+
if new_count > 0:
|
|
653
|
+
self.title_label.setText(f"📋 等待回复({count}) / 新反馈({new_count})")
|
|
654
|
+
if not self.has_new_feedback:
|
|
655
|
+
self.has_new_feedback = True
|
|
656
|
+
self.glow_timer.start(50)
|
|
657
|
+
else:
|
|
658
|
+
self.title_label.setText(f"📋 等待回复 ({count})")
|
|
659
|
+
if self.has_new_feedback:
|
|
660
|
+
self.has_new_feedback = False
|
|
661
|
+
self.glow_timer.stop()
|
|
662
|
+
self.glow_phase = 0
|
|
663
|
+
self.title_bar.setStyleSheet("""
|
|
664
|
+
QWidget#titleBar {
|
|
665
|
+
background-color: rgba(60, 60, 60, 255);
|
|
666
|
+
border-top-left-radius: 15px;
|
|
667
|
+
border-top-right-radius: 15px;
|
|
668
|
+
}
|
|
669
|
+
QLabel#titleLabel {
|
|
670
|
+
color: white;
|
|
671
|
+
font-size: 13px;
|
|
672
|
+
font-weight: bold;
|
|
673
|
+
background-color: transparent;
|
|
674
|
+
border: none;
|
|
675
|
+
}
|
|
676
|
+
QPushButton#collapseButton {
|
|
677
|
+
background-color: transparent;
|
|
678
|
+
color: rgba(255, 255, 255, 180);
|
|
679
|
+
border: none;
|
|
680
|
+
font-size: 12px;
|
|
681
|
+
}
|
|
682
|
+
""")
|
|
683
|
+
|
|
684
|
+
# 清空现有会话项
|
|
685
|
+
while self.session_layout.count() > 1: # 保留最后的stretch
|
|
686
|
+
item = self.session_layout.takeAt(0)
|
|
687
|
+
if item.widget():
|
|
688
|
+
item.widget().deleteLater()
|
|
689
|
+
|
|
690
|
+
# 添加新会话项
|
|
691
|
+
for session in sessions:
|
|
692
|
+
session_item = self._create_session_item(session)
|
|
693
|
+
self.session_layout.insertWidget(self.session_layout.count() - 1, session_item)
|
|
694
|
+
|
|
695
|
+
# 如果没有会话,隐藏窗口
|
|
696
|
+
if count == 0:
|
|
697
|
+
self.hide()
|
|
698
|
+
else:
|
|
699
|
+
self.show()
|
|
700
|
+
|
|
701
|
+
except Exception as e:
|
|
702
|
+
print(f"更新会话列表失败: {e}")
|
|
703
|
+
|
|
704
|
+
def _set_position(self):
|
|
705
|
+
"""设置窗口位置 - 屏幕右侧,距右边缘20px"""
|
|
706
|
+
screen = QGuiApplication.primaryScreen()
|
|
707
|
+
if screen:
|
|
708
|
+
screen_geometry = screen.availableGeometry()
|
|
709
|
+
screen_width = screen_geometry.width()
|
|
710
|
+
screen_height = screen_geometry.height()
|
|
711
|
+
screen_x = screen_geometry.x()
|
|
712
|
+
screen_y = screen_geometry.y()
|
|
713
|
+
else:
|
|
714
|
+
screen_width = 1920
|
|
715
|
+
screen_height = 1080
|
|
716
|
+
screen_x = 0
|
|
717
|
+
screen_y = 0
|
|
718
|
+
|
|
719
|
+
# 窗口尺寸
|
|
720
|
+
window_width = 320
|
|
721
|
+
window_height = 400
|
|
722
|
+
margin = 20
|
|
723
|
+
|
|
724
|
+
# 计算位置:右侧,垂直居中
|
|
725
|
+
x = screen_x + screen_width - window_width - margin
|
|
726
|
+
y = screen_y + (screen_height - window_height) // 2
|
|
727
|
+
|
|
728
|
+
self.move(x, y)
|
|
729
|
+
|
|
730
|
+
def mousePressEvent(self, event):
|
|
731
|
+
"""处理鼠标按下事件 - 记录起始状态"""
|
|
732
|
+
if event.button() == Qt.LeftButton:
|
|
733
|
+
self.dragging = False
|
|
734
|
+
self.mouse_press_time = time.time()
|
|
735
|
+
self.mouse_press_pos = event.globalPosition().toPoint()
|
|
736
|
+
self.drag_start_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
|
737
|
+
event.accept()
|
|
738
|
+
|
|
739
|
+
def mouseMoveEvent(self, event):
|
|
740
|
+
"""处理鼠标移动事件 - 拖动窗口"""
|
|
741
|
+
if event.buttons() == Qt.LeftButton and self.mouse_press_pos:
|
|
742
|
+
current_pos = event.globalPosition().toPoint()
|
|
743
|
+
distance = (current_pos - self.mouse_press_pos).manhattanLength()
|
|
744
|
+
# 移动距离超过5像素才开始拖动
|
|
745
|
+
if distance > 5:
|
|
746
|
+
self.dragging = True
|
|
747
|
+
self.move(current_pos - self.drag_start_pos)
|
|
748
|
+
event.accept()
|
|
749
|
+
|
|
750
|
+
def mouseReleaseEvent(self, event):
|
|
751
|
+
"""处理鼠标释放事件 - 判断点击或拖动"""
|
|
752
|
+
if event.button() == Qt.LeftButton and self.mouse_press_pos:
|
|
753
|
+
elapsed = time.time() - self.mouse_press_time
|
|
754
|
+
current_pos = event.globalPosition().toPoint()
|
|
755
|
+
distance = (current_pos - self.mouse_press_pos).manhattanLength()
|
|
756
|
+
# 短按且移动距离小 = 点击,触发展开/收起
|
|
757
|
+
if elapsed < 0.3 and distance < 5:
|
|
758
|
+
# 检查点击位置是否在标题栏区域
|
|
759
|
+
title_bar_rect = self.title_bar.geometry()
|
|
760
|
+
click_pos = event.position().toPoint()
|
|
761
|
+
if click_pos.y() < title_bar_rect.height():
|
|
762
|
+
self._toggle_collapse()
|
|
763
|
+
self.dragging = False
|
|
764
|
+
self.mouse_press_pos = None
|
|
765
|
+
event.accept()
|
|
766
|
+
|
|
767
|
+
def closeEvent(self, event):
|
|
768
|
+
"""窗口关闭事件"""
|
|
769
|
+
# 停止定时器
|
|
770
|
+
if hasattr(self, 'update_timer'):
|
|
771
|
+
self.update_timer.stop()
|
|
772
|
+
|
|
773
|
+
# 清理Socket连接
|
|
774
|
+
with self.sessions_lock:
|
|
775
|
+
for client_socket in self.session_sockets.values():
|
|
776
|
+
try:
|
|
777
|
+
client_socket.close()
|
|
778
|
+
except:
|
|
779
|
+
pass
|
|
780
|
+
self.session_sockets.clear()
|
|
781
|
+
self.sessions.clear()
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
event.accept()
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
if __name__ == "__main__":
|
|
788
|
+
from PySide6.QtWidgets import QApplication
|
|
789
|
+
|
|
790
|
+
app = QApplication(sys.argv)
|
|
791
|
+
window = SessionListUI()
|
|
792
|
+
window.show()
|
|
793
|
+
sys.exit(app.exec())
|