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.

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())