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.

feedback_ui.py ADDED
@@ -0,0 +1,1680 @@
1
+ """
2
+ 重构后的反馈UI - 使用模块化架构
3
+ """
4
+ import os
5
+ import sys
6
+ import json
7
+ import argparse
8
+ import base64
9
+ # 移除不必要的导入
10
+ # import markdown - 未使用
11
+ # import requests - 未使用
12
+ # import yaml - 未使用
13
+ # import glob - 未使用
14
+ # from io import BytesIO - 未使用
15
+ # from datetime import datetime, timedelta - 未使用
16
+ # from pathlib import Path - 未使用
17
+ from typing import Optional, TypedDict, List, Dict
18
+
19
+ # 导入窗口位置管理器
20
+ try:
21
+ from window_position_manager import WindowPositionManager
22
+ except ImportError:
23
+ WindowPositionManager = None
24
+
25
+ from PySide6.QtWidgets import (
26
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
27
+ QLabel, QPushButton, QProgressBar, QTabWidget, QMessageBox
28
+ )
29
+ from PySide6.QtCore import Qt, QTimer, QSettings, Signal, QThread
30
+ from PySide6.QtGui import QPalette, QColor, QGuiApplication
31
+
32
+ # 导入统一日志系统
33
+ from debug_logger import get_debug_logger
34
+ from session_manager import SessionManager
35
+
36
+ # 导入IDE工具
37
+ try:
38
+ from ide_utils import focus_cursor_to_project, is_macos
39
+ except ImportError:
40
+ # 如果导入失败,设置默认函数
41
+ def focus_cursor_to_project(project_path: str) -> bool:
42
+ return False
43
+ def is_macos() -> bool:
44
+ return False
45
+
46
+ # 导入模块化组件 - 修复PyArmor加密环境下的导入问题
47
+ import sys
48
+ import os
49
+
50
+ # 确保当前目录在Python路径中
51
+ current_dir = os.path.dirname(os.path.abspath(__file__))
52
+ if current_dir not in sys.path:
53
+ sys.path.insert(0, current_dir)
54
+
55
+ try:
56
+ # 只导入必要的ChatTab、WorkspaceTab和ChatHistoryTab
57
+ from tabs import ChatTab, WorkspaceTab, ChatHistoryTab
58
+ except ImportError as e:
59
+ # 如果导入失败,设置为None
60
+ ChatTab = None
61
+ WorkspaceTab = None
62
+ ChatHistoryTab = None
63
+
64
+
65
+
66
+
67
+
68
+ class FeedbackResult(TypedDict):
69
+ content: List[Dict[str, str]] # 结构化内容数组,每个元素包含type和text
70
+ images: Optional[List[str]] # Base64 encoded images
71
+
72
+ class VersionCheckThread(QThread):
73
+ """版本检查线程 - 在独立线程中执行网络请求"""
74
+
75
+ # 定义信号:参数为(latest_version, current_version)
76
+ version_checked = Signal(str, str)
77
+
78
+ def __init__(self, current_version: str, parent=None):
79
+ super().__init__(parent)
80
+ self.current_version = current_version
81
+ self._stop_requested = False
82
+
83
+ def request_stop(self):
84
+ """请求停止线程"""
85
+ self._stop_requested = True
86
+
87
+ def run(self):
88
+ """在独立线程中执行版本检查"""
89
+ try:
90
+ if self._stop_requested:
91
+ return
92
+ import requests
93
+ resp = requests.get('https://pypi.org/pypi/feedback-mcp/json', timeout=5)
94
+ if self._stop_requested:
95
+ return
96
+ if resp.status_code == 200:
97
+ latest = resp.json()['info']['version']
98
+ # 发送信号到主线程
99
+ self.version_checked.emit(latest, self.current_version)
100
+ except Exception:
101
+ # 静默处理错误,不发送信号
102
+ pass
103
+
104
+
105
+ def get_dark_mode_palette(app: QApplication):
106
+ darkPalette = app.palette()
107
+ darkPalette.setColor(QPalette.Window, QColor(53, 53, 53))
108
+ darkPalette.setColor(QPalette.WindowText, Qt.white)
109
+ darkPalette.setColor(QPalette.Disabled, QPalette.WindowText, QColor(127, 127, 127))
110
+ darkPalette.setColor(QPalette.Base, QColor(42, 42, 42))
111
+ darkPalette.setColor(QPalette.AlternateBase, QColor(66, 66, 66))
112
+ darkPalette.setColor(QPalette.ToolTipBase, QColor(53, 53, 53))
113
+ darkPalette.setColor(QPalette.ToolTipText, Qt.white)
114
+ darkPalette.setColor(QPalette.Text, Qt.white)
115
+ darkPalette.setColor(QPalette.Disabled, QPalette.Text, QColor(127, 127, 127))
116
+ darkPalette.setColor(QPalette.Dark, QColor(35, 35, 35))
117
+ darkPalette.setColor(QPalette.Shadow, QColor(20, 20, 20))
118
+ darkPalette.setColor(QPalette.Button, QColor(53, 53, 53))
119
+ darkPalette.setColor(QPalette.ButtonText, Qt.white)
120
+ darkPalette.setColor(QPalette.Disabled, QPalette.ButtonText, QColor(127, 127, 127))
121
+ darkPalette.setColor(QPalette.BrightText, Qt.red)
122
+ darkPalette.setColor(QPalette.Link, QColor(42, 130, 218))
123
+ darkPalette.setColor(QPalette.Highlight, QColor(42, 130, 218))
124
+ darkPalette.setColor(QPalette.Disabled, QPalette.Highlight, QColor(80, 80, 80))
125
+ darkPalette.setColor(QPalette.HighlightedText, Qt.white)
126
+ darkPalette.setColor(QPalette.Disabled, QPalette.HighlightedText, QColor(127, 127, 127))
127
+ darkPalette.setColor(QPalette.PlaceholderText, QColor(127, 127, 127))
128
+ return darkPalette
129
+
130
+
131
+ class FeedbackUI(QMainWindow):
132
+ """重构后的反馈UI主界面"""
133
+
134
+ def __init__(self, prompt: str, predefined_options: Optional[List[str]] = None, project_path: Optional[str] = None, work_title: Optional[str] = None, timeout: int = 60, skip_auth_check: bool = False, skip_init_check: bool = False, session_id: Optional[str] = None, workspace_id: Optional[str] = None, files: Optional[List[str]] = None, bugdetail: Optional[str] = None, ide: Optional[str] = None):
135
+ super().__init__()
136
+
137
+ # 基本参数
138
+ self.prompt = prompt
139
+ self.predefined_options = predefined_options or []
140
+ self.project_path = project_path
141
+ self.work_title = work_title or ""
142
+ self.timeout = timeout
143
+ self.skip_init_check = skip_init_check
144
+ self.elapsed_time = 0
145
+ self.session_id = session_id # 保存会话ID
146
+ self.workspace_id = workspace_id # 保存工作空间ID
147
+ self.files = files or [] # 保存文件列表
148
+ self.bugdetail = bugdetail # 保存bug详情
149
+ self.ide = ide # 保存指定的IDE
150
+
151
+ # 如果传入了IDE参数,设置环境变量以便其他模块使用
152
+ if ide:
153
+ os.environ['IDE'] = ide
154
+ try:
155
+ logger = get_debug_logger()
156
+ logger.info(f"设置IDE环境变量: {ide}")
157
+ except:
158
+ pass # 忽略日志错误
159
+
160
+ # 展示feedback时,重置stop hook状态
161
+ if self.session_id:
162
+ try:
163
+ manager = SessionManager(session_id=self.session_id, project_path=self.project_path)
164
+ manager.reset_on_feedback_show(self.session_id)
165
+ except Exception as e:
166
+ try:
167
+ logger = get_debug_logger()
168
+ logger.log_warning(f"Failed to reset stop hook state: {e}", "UI")
169
+ except:
170
+ pass # 忽略日志错误
171
+
172
+ # 结果存储
173
+ self.feedback_result = None
174
+ self.is_temp_close = False # 临时关闭标记(精简版按钮)
175
+
176
+ # 定时器
177
+ self.countdown_timer = QTimer()
178
+ self.countdown_timer.timeout.connect(self._update_countdown)
179
+ self.countdown_timer.setSingleShot(False) # 确保定时器可以重复触发
180
+
181
+ # 双击ESC关闭的计时器
182
+ self.esc_timer = QTimer()
183
+ self.esc_timer.setSingleShot(True)
184
+ self.esc_timer.timeout.connect(self._reset_esc_count)
185
+ self.esc_press_count = 0
186
+
187
+ # UI组件
188
+ self.main_tab_widget = None
189
+ self.chat_tab = None
190
+ self.chat_history_tab = None
191
+ self.memory_tab = None
192
+ self.rules_tab = None
193
+ self.todos_tab = None
194
+ self.checkpoints_tab = None
195
+ self.stats_tab = None
196
+ self.workflow_tab = None
197
+ self.taskflow_tab = None
198
+ self.new_work_tab = None
199
+
200
+ # 设置窗口
201
+ if project_path:
202
+ project_name = os.path.basename(os.path.normpath(project_path))
203
+ if self.work_title:
204
+ self.setWindowTitle(f"{project_name} - {self.work_title}")
205
+ else:
206
+ self.setWindowTitle(project_name)
207
+ else:
208
+ if self.work_title:
209
+ self.setWindowTitle(f"Interactive Feedback - {self.work_title}")
210
+ else:
211
+ self.setWindowTitle("Interactive Feedback")
212
+ self.setMinimumSize(550, 600)
213
+ self.resize(550, 900)
214
+
215
+ # 设置窗口始终置顶
216
+ from PySide6.QtCore import Qt
217
+ self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
218
+
219
+ # 检查项目初始化状态
220
+ self.project_initialized = True if skip_init_check else self._check_project_initialization()
221
+
222
+ # 直接创建UI,不再进行认证检查
223
+ self._create_ui()
224
+
225
+ # 设置智能窗口位置(避免重叠)
226
+ self._set_smart_position()
227
+
228
+ # Start countdown timer (无论认证状态如何都启动)
229
+ if self.timeout > 0:
230
+ try:
231
+ self.countdown_timer.start(1000) # Update every second
232
+ except Exception as e:
233
+ logger = get_debug_logger()
234
+ logger.log_error(f"启动倒计时器失败: {e}", "UI")
235
+
236
+ # 设置快捷键
237
+ self._setup_shortcuts()
238
+
239
+ # 初始化版本检查线程
240
+ self.version_check_thread = None
241
+
242
+ # 30秒后在独立线程中检查新版本
243
+ if self.timeout > 0:
244
+ QTimer.singleShot(30000, self._start_version_check)
245
+
246
+ def _get_version(self):
247
+ """获取版本号 - 优先从包元数据读取,然后从文件读取"""
248
+ # 方案1: 从包元数据读取(适用于pip安装后)
249
+ try:
250
+ from importlib.metadata import version
251
+ return version('feedback-mcp')
252
+ except Exception:
253
+ pass
254
+
255
+ # 方案2: 从version.txt读取(适用于开发环境)
256
+ try:
257
+ from pathlib import Path
258
+ version_file = Path(__file__).parent.parent / 'version.txt'
259
+ if version_file.exists():
260
+ return version_file.read_text().strip()
261
+ except Exception:
262
+ pass
263
+
264
+ # 方案3: 从pyproject.toml读取(适用于开发环境)
265
+ try:
266
+ from pathlib import Path
267
+ pyproject_file = Path(__file__).parent.parent / 'pyproject.toml'
268
+ if pyproject_file.exists():
269
+ content = pyproject_file.read_text()
270
+ for line in content.split('\n'):
271
+ if line.startswith('version ='):
272
+ return line.split('=')[1].strip().strip('"')
273
+ except Exception:
274
+ pass
275
+
276
+ # 最终降级方案
277
+ return "1.0.0"
278
+
279
+ def _start_version_check(self):
280
+ """启动版本检查线程"""
281
+ try:
282
+ current_version = self._get_version()
283
+ # 创建并启动版本检查线程
284
+ self.version_check_thread = VersionCheckThread(current_version) # 移除 parent
285
+ # 连接信号到槽函数
286
+ self.version_check_thread.version_checked.connect(self._on_version_checked)
287
+ # 线程结束后清理引用并删除对象
288
+ self.version_check_thread.finished.connect(self._on_version_check_finished)
289
+ # 启动线程
290
+ self.version_check_thread.start()
291
+ except Exception:
292
+ pass # 静默处理错误
293
+
294
+ def _on_version_check_finished(self):
295
+ """版本检查线程结束的回调"""
296
+ try:
297
+ if self.version_check_thread:
298
+ self.version_check_thread.deleteLater()
299
+ self.version_check_thread = None
300
+ except Exception:
301
+ pass
302
+
303
+ def _on_version_checked(self, latest: str, current: str):
304
+ """版本检查完成的回调(在主线程中执行)"""
305
+ try:
306
+ # 版本比较:只有当latest > current时才提示更新
307
+ if self._version_compare(latest, current) > 0:
308
+ # 更新版本标签文本和样式
309
+ self.version_label.setText(f"当前版本 {current} | 🔔 有新版本 {latest}")
310
+ self.version_label.setStyleSheet("""
311
+ QLabel {
312
+ color: #4CAF50;
313
+ font-size: 10px;
314
+ padding: 2px 6px;
315
+ text-decoration: underline;
316
+ }
317
+ """)
318
+ # 更新tooltip,提示可以点击
319
+ self.version_label.setToolTip(f"发现新版本 v{latest}\n点击复制更新命令")
320
+ # 设置鼠标指针为手型
321
+ self.version_label.setCursor(Qt.PointingHandCursor)
322
+ # 启用鼠标事件
323
+ self.version_label.setMouseTracking(True)
324
+ # 保存最新版本号,供点击事件使用
325
+ self.latest_version = latest
326
+ # 添加点击事件
327
+ self.version_label.mousePressEvent = self._on_version_label_clicked
328
+ except Exception:
329
+ pass # 静默处理错误
330
+
331
+ def _on_version_label_clicked(self, event):
332
+ """处理版本标签点击事件 - 复制更新命令并弹窗提示"""
333
+ from PySide6.QtWidgets import QApplication, QMessageBox
334
+ if hasattr(self, 'latest_version'):
335
+ # 复制更新命令到剪贴板
336
+ update_command = f"pip install --upgrade feedback-mcp"
337
+ QApplication.clipboard().setText(update_command)
338
+
339
+ # 显示弹窗提示
340
+ msg_box = QMessageBox(self)
341
+ msg_box.setWindowTitle("版本更新")
342
+ msg_box.setIcon(QMessageBox.Icon.Information)
343
+ msg_box.setText(f"已复制更新指令到剪贴板,请升级\n\n更新命令:{update_command}")
344
+ msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
345
+
346
+ # 应用暗色主题样式
347
+ msg_box.setStyleSheet("""
348
+ QMessageBox {
349
+ background-color: #2b2b2b;
350
+ color: #ffffff;
351
+ }
352
+ QMessageBox QLabel {
353
+ color: #ffffff;
354
+ }
355
+ QMessageBox QPushButton {
356
+ background-color: #3c3c3c;
357
+ color: #ffffff;
358
+ border: 1px solid #555555;
359
+ padding: 5px 15px;
360
+ border-radius: 3px;
361
+ }
362
+ QMessageBox QPushButton:hover {
363
+ background-color: #4a4a4a;
364
+ }
365
+ QMessageBox QPushButton:pressed {
366
+ background-color: #2a2a2a;
367
+ }
368
+ """)
369
+
370
+ msg_box.exec()
371
+
372
+ def _check_project_initialization(self) -> bool:
373
+ """检查项目是否已初始化(检查.agent和_agent-local目录是否存在)"""
374
+ if not self.project_path:
375
+ return False
376
+
377
+ agent_dir = os.path.join(self.project_path, ".agent")
378
+ agent_local_dir = os.path.join(self.project_path, "_agent-local")
379
+
380
+ return os.path.exists(agent_dir) and os.path.exists(agent_local_dir)
381
+
382
+ def _create_initialization_status_widget(self, header_layout):
383
+ """创建项目初始化状态显示组件"""
384
+ if not self.project_path:
385
+ return
386
+
387
+ # 如果跳过初始化检查,不显示初始化组件
388
+ if self.skip_init_check:
389
+ return
390
+
391
+ # 只有未初始化时才显示组件,已初始化时保持界面简洁
392
+ if not self.project_initialized:
393
+ # 未初始化,显示初始化按钮,样式与其他header按钮保持一致
394
+ init_button = QPushButton("项目初始化")
395
+ init_button.setMaximumWidth(100)
396
+ init_button.clicked.connect(self._show_initialization_command)
397
+ # 使用与精简版按钮相同的样式风格,但使用警告色调
398
+ init_button.setStyleSheet("""
399
+ QPushButton {
400
+ background-color: #FF9800;
401
+ color: white;
402
+ border: none;
403
+ padding: 4px 8px;
404
+ border-radius: 3px;
405
+ font-size: 11px;
406
+ }
407
+ QPushButton:hover {
408
+ background-color: #F57C00;
409
+ }
410
+ QPushButton:pressed {
411
+ background-color: #E65100;
412
+ }
413
+ """)
414
+ header_layout.addWidget(init_button)
415
+
416
+ def _show_initialization_dialog(self):
417
+ """显示项目初始化提示弹窗(优化版:去除延迟,直接显示)"""
418
+ from PySide6.QtWidgets import QMessageBox
419
+
420
+ msg_box = QMessageBox(self)
421
+ msg_box.setWindowTitle("项目未初始化")
422
+ msg_box.setIcon(QMessageBox.Icon.Warning)
423
+
424
+ # 将详细信息直接放在主文本中,不使用详细文本
425
+ main_text = """检测到当前项目尚未初始化"""
426
+
427
+ msg_box.setText(main_text)
428
+ msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
429
+
430
+ # 应用暗色主题样式
431
+ msg_box.setStyleSheet("""
432
+ QMessageBox {
433
+ background-color: #2b2b2b;
434
+ color: #ffffff;
435
+ }
436
+ QMessageBox QLabel {
437
+ color: #ffffff;
438
+ }
439
+ QMessageBox QPushButton {
440
+ background-color: #3c3c3c;
441
+ color: #ffffff;
442
+ border: 1px solid #555555;
443
+ padding: 5px 15px;
444
+ border-radius: 3px;
445
+ }
446
+ QMessageBox QPushButton:hover {
447
+ background-color: #4a4a4a;
448
+ }
449
+ QMessageBox QPushButton:pressed {
450
+ background-color: #2a2a2a;
451
+ }
452
+ """)
453
+
454
+ # 直接显示对话框,去除延迟
455
+ try:
456
+ result = msg_box.exec()
457
+ if result == QMessageBox.StandardButton.Ok:
458
+ # 用户点击确定,自动发送初始化命令反馈
459
+ init_message = "请执行命令初始化该项目的AI工具 npm exec --registry=https://omp-npm.acewill.net/ -- workflow-mcp-init"
460
+ self.feedback_result = {
461
+ 'content': [{"type": "text", "text": init_message}],
462
+ 'images': []
463
+ }
464
+ # 关闭当前窗口,返回反馈
465
+ self.close()
466
+ except Exception as e:
467
+ logger = get_debug_logger()
468
+ logger.log_error(f"显示初始化对话框失败: {e}", "UI")
469
+
470
+ def _show_initialization_command(self):
471
+ """显示初始化命令信息对话框"""
472
+ from PySide6.QtWidgets import QMessageBox
473
+
474
+ msg_box = QMessageBox(self)
475
+ msg_box.setWindowTitle("项目初始化")
476
+ msg_box.setIcon(QMessageBox.Icon.Information)
477
+
478
+ command_text = "npm exec --registry=https://omp-npm.acewill.net/ -- workflow-mcp-init"
479
+
480
+ # 将详细信息直接放在主文本中,不使用详细文本
481
+ main_text = f"""请在项目根目录下执行以下命令:
482
+
483
+ {command_text}
484
+
485
+ 命令执行完成后,将会创建以下目录:
486
+ • .agent/ - 代理配置目录
487
+ • _agent-local/ - 本地代理数据目录
488
+
489
+ 初始化完成后,请重新打开此界面以使用完整功能。"""
490
+
491
+ msg_box.setText(main_text)
492
+ msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
493
+
494
+ # 应用暗色主题样式
495
+ msg_box.setStyleSheet("""
496
+ QMessageBox {
497
+ background-color: #2b2b2b;
498
+ color: #ffffff;
499
+ }
500
+ QMessageBox QLabel {
501
+ color: #ffffff;
502
+ }
503
+ QMessageBox QPushButton {
504
+ background-color: #3c3c3c;
505
+ color: #ffffff;
506
+ border: 1px solid #555555;
507
+ padding: 5px 15px;
508
+ border-radius: 3px;
509
+ }
510
+ QMessageBox QPushButton:hover {
511
+ background-color: #4a4a4a;
512
+ }
513
+ QMessageBox QPushButton:pressed {
514
+ background-color: #2a2a2a;
515
+ }
516
+ """)
517
+
518
+ result = msg_box.exec()
519
+ if result == QMessageBox.StandardButton.Ok:
520
+ # 用户点击确定,自动发送初始化命令反馈
521
+ init_message = "请执行命令初始化该项目的AI工具 npm exec --registry=https://omp-npm.acewill.net/ -- workflow-mcp-init"
522
+ self.feedback_result = {
523
+ 'content': [{"type": "text", "text": init_message}],
524
+ 'images': []
525
+ }
526
+ # 关闭当前窗口,返回反馈
527
+ self.close()
528
+
529
+ def _create_ui(self):
530
+ """创建主界面"""
531
+ # 设置中央部件
532
+ central_widget = QWidget()
533
+ self.setCentralWidget(central_widget)
534
+
535
+ # 主布局
536
+ layout = QVBoxLayout(central_widget)
537
+ layout.setContentsMargins(10, 10, 10, 10)
538
+
539
+ # 添加状态栏
540
+ self.statusBar().showMessage("就绪", 2000)
541
+
542
+ # Header with GitLab auth status
543
+ header_layout = QHBoxLayout()
544
+
545
+ # 版本号标签 - 左上角
546
+ self.version_label = QLabel(f"v{self._get_version()}")
547
+ self.version_label.setStyleSheet("""
548
+ QLabel {
549
+ color: #888888;
550
+ font-size: 9px;
551
+ padding: 2px 6px;
552
+ }
553
+ """)
554
+ self.version_label.setToolTip("当前版本")
555
+ header_layout.addWidget(self.version_label)
556
+
557
+ # 已移除GitLab认证状态显示
558
+
559
+ # 项目初始化状态显示
560
+ self._create_initialization_status_widget(header_layout)
561
+
562
+ header_layout.addStretch() # Push content to center
563
+
564
+ # IDE设置按钮(放在注销按钮右侧,显示IDE按钮左侧)
565
+ self.ide_settings_button = QPushButton("设置IDE")
566
+ self.ide_settings_button.setMaximumWidth(80)
567
+ self.ide_settings_button.clicked.connect(self._show_ide_settings_dialog)
568
+ self.ide_settings_button.setStyleSheet("""
569
+ QPushButton {
570
+ background-color: #2196F3;
571
+ color: white;
572
+ border: none;
573
+ padding: 4px 8px;
574
+ border-radius: 3px;
575
+ font-size: 11px;
576
+ }
577
+ QPushButton:hover {
578
+ background-color: #1976D2;
579
+ }
580
+ QPushButton:pressed {
581
+ background-color: #0D47A1;
582
+ }
583
+ """)
584
+ self.ide_settings_button.setToolTip("设置默认IDE")
585
+ header_layout.addWidget(self.ide_settings_button)
586
+
587
+ # 显示IDE按钮
588
+ # 优先使用配置文件,其次使用传入的ide参数(环境变量)
589
+ # DEBUG: 打印IDE参数状态
590
+ print(f"[DEBUG] FeedbackUI初始化 - self.ide={self.ide}, 环境变量IDE={os.getenv('IDE')}")
591
+
592
+ # 尝试从配置文件读取IDE
593
+ ide_from_config = None
594
+ if self.project_path:
595
+ try:
596
+ from feedback_config import FeedbackConfig
597
+ config_manager = FeedbackConfig(self.project_path)
598
+ ide_from_config = config_manager.get_ide()
599
+ except Exception:
600
+ pass # 忽略错误,使用默认值
601
+
602
+ # 确定最终使用的IDE:配置文件 > 环境变量参数 > 默认
603
+ final_ide = ide_from_config or self.ide
604
+
605
+ if final_ide:
606
+ # 动态生成IDE显示名称
607
+ # 如果IDE名称全小写,则首字母大写;否则保留原样
608
+ ide_display_name = final_ide if any(c.isupper() for c in final_ide) else final_ide.capitalize()
609
+ if final_ide.lower() == "vscode":
610
+ ide_display_name = "VSCode"
611
+ try:
612
+ logger = get_debug_logger()
613
+ logger.info(f"使用IDE: {final_ide} -> 显示名称: {ide_display_name}")
614
+ except:
615
+ pass # 忽略日志错误
616
+ else:
617
+ # 没有配置IDE
618
+ ide_display_name = "IDE"
619
+ try:
620
+ logger = get_debug_logger()
621
+ logger.info("未配置IDE")
622
+ except:
623
+ pass # 忽略日志错误
624
+
625
+ self.ide_button = QPushButton(f"打开{ide_display_name}")
626
+ self.ide_button.setMaximumWidth(100)
627
+ self.ide_button.clicked.connect(self._open_cursor_ide)
628
+ self.ide_button.setStyleSheet("""
629
+ QPushButton {
630
+ background-color: #4CAF50;
631
+ color: white;
632
+ border: none;
633
+ padding: 4px 8px;
634
+ border-radius: 3px;
635
+ font-size: 11px;
636
+ }
637
+ QPushButton:hover {
638
+ background-color: #45a049;
639
+ }
640
+ QPushButton:pressed {
641
+ background-color: #3d8b40;
642
+ }
643
+ """)
644
+ self.ide_button.setToolTip(f"使用 {ide_display_name} 打开当前项目")
645
+ header_layout.addWidget(self.ide_button)
646
+
647
+ # 稍后处理按钮(临时关闭)
648
+ self.compact_button = QPushButton("稍后处理")
649
+ self.compact_button.setMaximumWidth(80)
650
+ self.compact_button.clicked.connect(self._temp_close)
651
+ self.compact_button.setStyleSheet("""
652
+ QPushButton {
653
+ background-color: #607D8B;
654
+ color: white;
655
+ border: none;
656
+ padding: 4px 8px;
657
+ border-radius: 3px;
658
+ font-size: 11px;
659
+ }
660
+ QPushButton:hover {
661
+ background-color: #546E7A;
662
+ }
663
+ QPushButton:pressed {
664
+ background-color: #455A64;
665
+ }
666
+ """)
667
+ header_layout.addWidget(self.compact_button)
668
+
669
+ layout.addLayout(header_layout)
670
+
671
+ # 创建标签页容器 - 与原版保持一致的命名
672
+ self.main_tab_widget = QTabWidget()
673
+ self.main_tab_widget.currentChanged.connect(self._on_main_tab_changed)
674
+
675
+ # 注意:与原版保持一致,不设置自定义样式,使用系统默认QTabWidget样式
676
+
677
+ # 只创建必要的对话标签页
678
+ self._create_chat_tab() # 反馈
679
+ self._create_chat_history_tab() # 对话记录
680
+
681
+ # 如果传入了workspace_id,创建工作空间tab
682
+ if self.workspace_id:
683
+ self._create_workspace_tab()
684
+
685
+ layout.addWidget(self.main_tab_widget)
686
+
687
+ # 🆕 如果项目未初始化,显示弹窗提示(与原版保持一致)
688
+ if not self.skip_init_check and not self.project_initialized:
689
+ self._show_initialization_dialog()
690
+
691
+ def _create_chat_tab(self):
692
+ """创建聊天标签页"""
693
+ self.chat_tab = ChatTab(
694
+ prompt=self.prompt,
695
+ predefined_options=self.predefined_options,
696
+ project_path=self.project_path,
697
+ work_title=self.work_title,
698
+ timeout=self.timeout,
699
+ files=self.files,
700
+ bugdetail=self.bugdetail,
701
+ session_id=self.session_id,
702
+ workspace_id=self.workspace_id,
703
+ parent=self
704
+ )
705
+
706
+ # 连接信号
707
+ self.chat_tab.feedback_submitted.connect(self._handle_feedback_submitted)
708
+ self.chat_tab.command_executed.connect(self._handle_command_execution)
709
+ self.chat_tab.option_executed.connect(self._execute_option_immediately)
710
+ self.chat_tab.text_changed.connect(self._on_text_changed)
711
+
712
+ self.main_tab_widget.addTab(self.chat_tab, "反馈")
713
+
714
+ def _create_chat_history_tab(self):
715
+ """创建对话记录标签页"""
716
+ if ChatHistoryTab:
717
+ self.chat_history_tab = ChatHistoryTab(
718
+ project_path=self.project_path,
719
+ session_id=self.session_id,
720
+ workspace_id=self.workspace_id,
721
+ parent=self
722
+ )
723
+ self.main_tab_widget.addTab(self.chat_history_tab, "对话记录")
724
+
725
+ def _create_workspace_tab(self):
726
+ """创建工作空间标签页
727
+
728
+ 只有在以下条件都满足时才创建工作空间tab:
729
+ 1. WorkspaceTab类可用
730
+ 2. 传入了workspace_id
731
+ 3. 能够成功加载工作空间配置
732
+ """
733
+ if not WorkspaceTab or not self.workspace_id:
734
+ return
735
+
736
+ # 验证是否能加载工作空间配置
737
+ try:
738
+ from workspace_manager import WorkspaceManager
739
+ manager = WorkspaceManager(self.project_path)
740
+ config = manager.load_workspace_config(self.workspace_id)
741
+
742
+ # 只有成功加载到配置时才创建tab
743
+ if config:
744
+ self.workspace_tab = WorkspaceTab(
745
+ workspace_id=self.workspace_id,
746
+ project_path=self.project_path,
747
+ parent=self
748
+ )
749
+ self.main_tab_widget.addTab(self.workspace_tab, "工作空间")
750
+ except Exception:
751
+ # 加载失败时不创建tab
752
+ pass
753
+
754
+ def _create_memory_tab(self):
755
+ """创建记忆选项卡"""
756
+ if MemoryTab and self.project_path:
757
+ self.memory_tab = MemoryTab(self.project_path, parent=self)
758
+ self.main_tab_widget.addTab(self.memory_tab, "记忆")
759
+
760
+ def _create_rules_tab(self):
761
+ """创建规则选项卡"""
762
+ if RulesTab and self.project_path:
763
+ self.rules_tab = RulesTab(self.project_path, parent=self)
764
+ self.main_tab_widget.addTab(self.rules_tab, "规则")
765
+
766
+ def _create_todos_tab_deprecated(self):
767
+ """创建Todos选项卡"""
768
+ # 确保正确导入TodosTab
769
+ try:
770
+ from tabs.todos_tab import TodosTab as LocalTodosTab
771
+ except ImportError:
772
+ LocalTodosTab = None
773
+
774
+ if LocalTodosTab and self.project_path:
775
+ try:
776
+ self.todos_tab = LocalTodosTab()
777
+ # 初始化项目路径
778
+ self.todos_tab.initialize_manager(self.project_path)
779
+ self.main_tab_widget.addTab(self.todos_tab, "Todos")
780
+ # 临时隐藏todos选项卡
781
+ self.todos_tab_index = self.main_tab_widget.count() - 1
782
+ self.main_tab_widget.setTabVisible(self.todos_tab_index, False)
783
+ except Exception as e:
784
+ self.todos_tab = None
785
+ else:
786
+ # 如果导入失败或没有项目路径,设置为None
787
+ self.todos_tab = None
788
+
789
+ def _create_checkpoints_tab_deprecated(self):
790
+ """创建检查点选项卡"""
791
+ if CheckpointsTab and self.project_path:
792
+ self.checkpoints_tab = CheckpointsTab(self.project_path, parent=self)
793
+ self.main_tab_widget.addTab(self.checkpoints_tab, "检查点")
794
+
795
+ def _create_workflow_tabs_deprecated(self):
796
+ """创建工作流相关标签页"""
797
+ # 当前工作流标签页
798
+ try:
799
+ current_workflow_tab = CurrentWorkflowWidget(project_path=self.project_path)
800
+ self.main_tab_widget.addTab(current_workflow_tab, "当前工作流")
801
+ self.current_workflow_tab_index = self.main_tab_widget.count() - 1
802
+ self.current_workflow_tab_widget = current_workflow_tab
803
+ except ImportError:
804
+ # 如果无法导入,创建空白标签页占位
805
+ from PySide6.QtWidgets import QWidget
806
+ current_workflow_tab = QWidget()
807
+ self.main_tab_widget.addTab(current_workflow_tab, "当前工作流")
808
+ self.current_workflow_tab_index = self.main_tab_widget.count() - 1
809
+ self.current_workflow_tab_widget = current_workflow_tab
810
+
811
+ # 当前任务流标签页
812
+ try:
813
+ current_taskflow_tab = CurrentTaskflowWidget(project_path=self.project_path)
814
+ self.main_tab_widget.addTab(current_taskflow_tab, "当前任务流")
815
+ self.current_taskflow_tab_index = self.main_tab_widget.count() - 1
816
+ self.current_taskflow_tab_widget = current_taskflow_tab
817
+ except ImportError:
818
+ # 如果无法导入,创建空白标签页占位
819
+ from PySide6.QtWidgets import QWidget
820
+ current_taskflow_tab = QWidget()
821
+ self.main_tab_widget.addTab(current_taskflow_tab, "当前任务流")
822
+ self.current_taskflow_tab_index = self.main_tab_widget.count() - 1
823
+ self.current_taskflow_tab_widget = current_taskflow_tab
824
+
825
+ # 注意:根据原版UI,默认只显示"对话"、"新工作"、"统计"三个标签页
826
+ # "当前工作流"和"当前任务流"标签页保持隐藏状态,但功能保留以备需要时显示
827
+ self.main_tab_widget.setTabVisible(self.current_workflow_tab_index, False)
828
+ self.main_tab_widget.setTabVisible(self.current_taskflow_tab_index, False)
829
+
830
+ def _create_new_project_tab_deprecated(self):
831
+ """创建新项目选项卡"""
832
+ if NewProjectTab:
833
+ self.new_project_tab = NewProjectTab(parent=self)
834
+ self.main_tab_widget.addTab(self.new_project_tab, "新项目")
835
+ # 临时隐藏新项目选项卡
836
+ self.new_project_tab_index = self.main_tab_widget.count() - 1
837
+ self.main_tab_widget.setTabVisible(self.new_project_tab_index, False)
838
+ else:
839
+ # 如果导入失败,设置为None
840
+ self.new_project_tab = None
841
+
842
+ def _create_new_work_tab(self):
843
+ """创建新工作标签页"""
844
+ self.new_work_tab = NewWorkTab(self.project_path, parent=self)
845
+
846
+ # 连接信号
847
+ self.new_work_tab.workflow_executed.connect(self._execute_workflow)
848
+ self.new_work_tab.taskflow_executed.connect(self._execute_taskflow)
849
+
850
+ self.main_tab_widget.addTab(self.new_work_tab, "新工作")
851
+
852
+ def _create_config_tab(self):
853
+ """创建配置标签页"""
854
+ # 配置功能已移除,IDE现在只从环境变量读取
855
+ from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout
856
+ config_widget = QWidget()
857
+ layout = QVBoxLayout(config_widget)
858
+ label = QLabel("IDE配置请通过环境变量设置\n\n例如: IDE=cursor")
859
+ label.setAlignment(Qt.AlignCenter)
860
+ label.setStyleSheet("color: #888888; font-size: 14px;")
861
+ layout.addWidget(label)
862
+ self.main_tab_widget.addTab(config_widget, "配置")
863
+
864
+ def _create_stats_tab(self):
865
+ """创建统计标签页"""
866
+ self.stats_tab = StatsTab(project_path=self.project_path, parent=self)
867
+ self.main_tab_widget.addTab(self.stats_tab, "统计")
868
+
869
+ def _on_ide_config_changed(self, ide_name: str):
870
+ """IDE配置变更时的处理"""
871
+ # IDE配置已改为从环境变量读取,此函数保留为空实现
872
+ pass
873
+
874
+ def _handle_feedback_submitted(self, content_parts: List[Dict[str, str]], images: List[str]):
875
+ """处理反馈提交"""
876
+ # 停止倒计时
877
+ if self.countdown_timer.isActive():
878
+ self.countdown_timer.stop()
879
+
880
+ # 用户正常提交,在后台清除stop hook状态
881
+ if self.session_id:
882
+ import threading
883
+ def clear_session_bg():
884
+ try:
885
+ manager = SessionManager(session_id=self.session_id, project_path=self.project_path)
886
+ manager.clear_session(self.session_id)
887
+ except Exception as e:
888
+ logger = get_debug_logger()
889
+ logger.log_warning(f"Failed to clear session on submit: {e}", "UI")
890
+ threading.Thread(target=clear_session_bg, daemon=True).start()
891
+
892
+ # 设置结果
893
+ self.feedback_result = {
894
+ 'content': content_parts,
895
+ 'images': images
896
+ }
897
+
898
+ self.close()
899
+
900
+ def _handle_command_execution(self, command_content: str):
901
+ """处理指令执行"""
902
+ if command_content:
903
+ # 构建指令内容的结构化格式
904
+ content_parts = [{"type": "command", "text": command_content}]
905
+
906
+ self.feedback_result = {
907
+ 'content': content_parts,
908
+ 'images': []
909
+ }
910
+ self.close()
911
+
912
+ def _execute_option_immediately(self, option_index: int):
913
+ """立即执行选项"""
914
+ if 0 <= option_index < len(self.predefined_options):
915
+ option_text = self.predefined_options[option_index]
916
+
917
+ content_parts = [{"type": "options", "text": option_text}]
918
+ self._handle_feedback_submitted(content_parts, [])
919
+
920
+ def _execute_workflow(self, workflow_name: str):
921
+ """执行工作流"""
922
+ command = f"/work use {workflow_name}"
923
+ self._handle_command_execution(command)
924
+
925
+ def _execute_taskflow(self, taskflow_name: str):
926
+ """执行任务流"""
927
+ command = f"/task use {taskflow_name}"
928
+ self._handle_command_execution(command)
929
+
930
+ def _on_text_changed(self):
931
+ """文本变化处理(委托给聊天标签页)"""
932
+ pass
933
+
934
+ def _on_main_tab_changed(self, index):
935
+ """主标签页切换处理 - 优化版:减少QTimer使用,改为直接同步调用"""
936
+ # 当切换到当前工作流选项卡时,直接刷新数据和显示
937
+ if hasattr(self, 'current_workflow_tab_index') and index == self.current_workflow_tab_index and hasattr(self, 'current_workflow_tab_widget'):
938
+ try:
939
+ # 直接刷新,不使用QTimer延迟
940
+ if hasattr(self.current_workflow_tab_widget, 'refresh_data'):
941
+ self.current_workflow_tab_widget.refresh_data()
942
+ self.current_workflow_tab_widget.show()
943
+ self.current_workflow_tab_widget.update()
944
+ except Exception as e:
945
+ logger = get_debug_logger()
946
+ logger.log_error(f"Error refreshing current workflow tab: {e}", "UI")
947
+
948
+ # 当切换到当前任务流选项卡时,直接刷新数据和显示
949
+ if hasattr(self, 'current_taskflow_tab_index') and index == self.current_taskflow_tab_index and hasattr(self, 'current_taskflow_tab_widget'):
950
+ try:
951
+ # 直接刷新,不使用QTimer延迟
952
+ if hasattr(self.current_taskflow_tab_widget, 'refresh_data'):
953
+ self.current_taskflow_tab_widget.refresh_data()
954
+ self.current_taskflow_tab_widget.show()
955
+ self.current_taskflow_tab_widget.update()
956
+ except Exception as e:
957
+ logger = get_debug_logger()
958
+ logger.log_error(f"Error refreshing current taskflow tab: {e}", "UI")
959
+
960
+ # 当切换到统计选项卡时,刷新数据(统计是最后一个选项卡)
961
+ if hasattr(self, 'stats_tab') and self.main_tab_widget.tabText(index) == "统计":
962
+ self.stats_tab.refresh_data()
963
+
964
+ def _open_cursor_ide(self):
965
+ """打开配置的IDE"""
966
+ try:
967
+ if not self.project_path:
968
+ self.statusBar().showMessage("❌ 请先选择项目路径", 3000)
969
+ return
970
+
971
+ # 获取当前配置的IDE
972
+ try:
973
+ from ide_utils import open_project_with_ide
974
+ from feedback_config import FeedbackConfig
975
+
976
+ # 优先从配置文件读取,其次使用传入的IDE参数(来自环境变量)
977
+ config_manager = FeedbackConfig(self.project_path)
978
+ ide_to_use = config_manager.get_ide() or self.ide
979
+
980
+ # 如果没有IDE配置,提示用户配置
981
+ if not ide_to_use:
982
+ reply = QMessageBox.question(
983
+ self,
984
+ "未配置IDE",
985
+ "尚未配置默认IDE,是否现在设置?",
986
+ QMessageBox.Yes | QMessageBox.No
987
+ )
988
+ if reply == QMessageBox.Yes:
989
+ self._show_ide_settings_dialog()
990
+ return
991
+
992
+ success = open_project_with_ide(self.project_path, ide_to_use)
993
+
994
+ # 动态获取IDE显示名称
995
+ # 如果是动态IDE,直接使用名称
996
+ if ide_to_use:
997
+ ide_display = ide_to_use if any(c.isupper() for c in ide_to_use) else ide_to_use.capitalize()
998
+ else:
999
+ ide_display = 'IDE'
1000
+
1001
+ if success:
1002
+ self.statusBar().showMessage(f"✅ {ide_display} 已打开", 3000)
1003
+ else:
1004
+ # 提供更详细的错误提示
1005
+ from ide_utils import is_ide_available
1006
+
1007
+ if not is_ide_available(ide_to_use):
1008
+ self.statusBar().showMessage(f"❌ {ide_display} 未安装或不在PATH中", 3000)
1009
+ else:
1010
+ self.statusBar().showMessage(f"❌ 打开 {ide_display} 失败", 3000)
1011
+
1012
+ except ImportError:
1013
+ # 回退到原来的Cursor逻辑
1014
+ success = focus_cursor_to_project(self.project_path)
1015
+ if success:
1016
+ self.statusBar().showMessage("✅ Cursor IDE 已打开", 3000)
1017
+ else:
1018
+ if not is_macos():
1019
+ self.statusBar().showMessage("❌ 此功能仅支持 macOS", 3000)
1020
+ else:
1021
+ self.statusBar().showMessage("❌ 打开 Cursor IDE 失败", 3000)
1022
+
1023
+ except Exception as e:
1024
+ self.statusBar().showMessage(f"❌ 打开IDE出错: {e}", 3000)
1025
+
1026
+ def _show_ide_settings_dialog(self):
1027
+ """显示IDE设置对话框"""
1028
+ from PySide6.QtWidgets import (
1029
+ QDialog, QVBoxLayout, QRadioButton, QLineEdit,
1030
+ QPushButton, QLabel, QButtonGroup, QHBoxLayout
1031
+ )
1032
+
1033
+ try:
1034
+ from feedback_config import FeedbackConfig
1035
+ except ImportError:
1036
+ QMessageBox.warning(self, "导入错误", "无法加载配置模块")
1037
+ return
1038
+
1039
+ dialog = QDialog(self)
1040
+ dialog.setWindowTitle("设置IDE")
1041
+ dialog.setMinimumWidth(400)
1042
+ layout = QVBoxLayout(dialog)
1043
+
1044
+ # 加载当前配置
1045
+ config_manager = FeedbackConfig(self.project_path)
1046
+ current_ide = config_manager.get_ide()
1047
+
1048
+ # 说明文字
1049
+ info_label = QLabel("选择默认IDE(用于打开项目):")
1050
+ layout.addWidget(info_label)
1051
+
1052
+ # 常用IDE单选按钮组
1053
+ button_group = QButtonGroup(dialog)
1054
+ button_group.setExclusive(False) # 允许取消勾选
1055
+ radio_buttons = {}
1056
+
1057
+ ides = ["cursor", "vscode", "kiro", "qoder", "pycharm", "intellij"]
1058
+ for ide in ides:
1059
+ rb = QRadioButton(ide.capitalize() if ide != "vscode" else "VSCode")
1060
+ rb.setProperty("ide_value", ide)
1061
+ radio_buttons[ide] = rb
1062
+ button_group.addButton(rb)
1063
+ layout.addWidget(rb)
1064
+
1065
+ # 如果当前配置匹配,选中该按钮
1066
+ if current_ide and current_ide.lower() == ide:
1067
+ rb.setChecked(True)
1068
+
1069
+ # 分隔线
1070
+ layout.addSpacing(10)
1071
+ separator_label = QLabel("或输入自定义IDE命令:")
1072
+ layout.addWidget(separator_label)
1073
+
1074
+ # 自定义IDE输入框
1075
+ custom_input = QLineEdit()
1076
+ custom_input.setPlaceholderText("例如:code, idea, sublime")
1077
+
1078
+ # 如果当前配置是自定义的,填充到输入框
1079
+ if current_ide and current_ide.lower() not in ides:
1080
+ custom_input.setText(current_ide)
1081
+
1082
+ layout.addWidget(custom_input)
1083
+
1084
+ # 添加交互联动
1085
+ def on_radio_clicked(clicked_button):
1086
+ """当点击单选按钮时的处理"""
1087
+ # 如果点击的是已选中的按钮,取消选中
1088
+ if clicked_button.isChecked():
1089
+ # 取消其他所有按钮的选中状态(实现互斥)
1090
+ for rb in radio_buttons.values():
1091
+ if rb != clicked_button:
1092
+ rb.setChecked(False)
1093
+ # 清空自定义输入框
1094
+ custom_input.clear()
1095
+
1096
+ def on_custom_input_changed():
1097
+ """当输入自定义命令时,取消所有预设单选按钮的选中"""
1098
+ if custom_input.text().strip():
1099
+ for rb in radio_buttons.values():
1100
+ rb.setChecked(False)
1101
+
1102
+ # 连接信号
1103
+ for rb in radio_buttons.values():
1104
+ rb.clicked.connect(lambda checked=False, btn=rb: on_radio_clicked(btn))
1105
+ custom_input.textChanged.connect(on_custom_input_changed)
1106
+
1107
+ # 按钮行
1108
+ button_layout = QHBoxLayout()
1109
+
1110
+ clear_button = QPushButton("清除配置")
1111
+ clear_button.clicked.connect(lambda: self._clear_ide_config(dialog, config_manager))
1112
+
1113
+ ok_button = QPushButton("确定")
1114
+ ok_button.setDefault(True)
1115
+
1116
+ cancel_button = QPushButton("取消")
1117
+
1118
+ button_layout.addWidget(clear_button)
1119
+ button_layout.addStretch()
1120
+ button_layout.addWidget(ok_button)
1121
+ button_layout.addWidget(cancel_button)
1122
+
1123
+ layout.addLayout(button_layout)
1124
+
1125
+ # 连接信号
1126
+ def save_and_close():
1127
+ ide_name = None
1128
+
1129
+ # 优先检查预设单选按钮
1130
+ selected_preset = None
1131
+ for ide, rb in radio_buttons.items():
1132
+ if rb.isChecked():
1133
+ selected_preset = ide
1134
+ break
1135
+
1136
+ if selected_preset:
1137
+ # 使用预设IDE
1138
+ config_manager.set_ide(ide=selected_preset)
1139
+ ide_name = selected_preset
1140
+ self.statusBar().showMessage(f"✅ IDE已设置为: {selected_preset.capitalize()}", 3000)
1141
+ else:
1142
+ # 检查自定义输入框
1143
+ custom_text = custom_input.text().strip()
1144
+ if custom_text:
1145
+ config_manager.set_ide(custom_command=custom_text)
1146
+ ide_name = custom_text
1147
+ self.statusBar().showMessage(f"✅ IDE已设置为: {custom_text}", 3000)
1148
+
1149
+ # 更新打开IDE按钮的文本
1150
+ if ide_name:
1151
+ ide_display = ide_name if any(c.isupper() for c in ide_name) else ide_name.capitalize()
1152
+ if ide_name.lower() == "vscode":
1153
+ ide_display = "VSCode"
1154
+ self.ide_button.setText(f"打开{ide_display}")
1155
+
1156
+ dialog.accept()
1157
+
1158
+ ok_button.clicked.connect(save_and_close)
1159
+ cancel_button.clicked.connect(dialog.reject)
1160
+
1161
+ # 应用暗色主题
1162
+ dialog.setStyleSheet("""
1163
+ QDialog {
1164
+ background-color: #2b2b2b;
1165
+ color: #ffffff;
1166
+ }
1167
+ QLabel {
1168
+ color: #ffffff;
1169
+ }
1170
+ QRadioButton {
1171
+ color: #ffffff;
1172
+ }
1173
+ QLineEdit {
1174
+ background-color: #3c3c3c;
1175
+ color: #ffffff;
1176
+ border: 1px solid #555555;
1177
+ padding: 5px;
1178
+ border-radius: 3px;
1179
+ }
1180
+ QPushButton {
1181
+ background-color: #3c3c3c;
1182
+ color: #ffffff;
1183
+ border: 1px solid #555555;
1184
+ padding: 5px 15px;
1185
+ border-radius: 3px;
1186
+ }
1187
+ QPushButton:hover {
1188
+ background-color: #4a4a4a;
1189
+ }
1190
+ QPushButton:pressed {
1191
+ background-color: #2a2a2a;
1192
+ }
1193
+ """)
1194
+
1195
+ dialog.exec()
1196
+
1197
+ def _clear_ide_config(self, dialog, config_manager):
1198
+ """清除IDE配置"""
1199
+ config_manager.clear_ide()
1200
+ self.statusBar().showMessage("✅ IDE配置已清除", 3000)
1201
+ # 恢复默认按钮文本
1202
+ self.ide_button.setText("打开IDE")
1203
+ dialog.accept()
1204
+
1205
+ def _check_updates(self):
1206
+ """检查更新"""
1207
+ import requests
1208
+ import subprocess
1209
+ from PySide6.QtWidgets import QMessageBox
1210
+
1211
+ try:
1212
+ # 获取GitLab认证
1213
+ if hasattr(self, 'auth_status_widget') and self.auth_status_widget:
1214
+ auth = self.auth_status_widget.auth
1215
+ if not auth.is_authenticated():
1216
+ QMessageBox.warning(self, "需要认证", "请先进行GitLab认证")
1217
+ return
1218
+ else:
1219
+ QMessageBox.warning(self, "认证错误", "无法获取GitLab认证状态")
1220
+ return
1221
+
1222
+ # 禁用按钮,防止重复点击
1223
+ self.update_button.setEnabled(False)
1224
+ self.update_button.setText("检查中...")
1225
+
1226
+ # 获取远程version.txt
1227
+ url = "https://gitlab.acewill.cn/api/v4/projects/ai%2Fagent-dev/repository/files/version.txt/raw?ref=3.5"
1228
+ headers = {"Authorization": f"Bearer {auth.load_token()}"}
1229
+
1230
+ response = requests.get(url, headers=headers, timeout=10)
1231
+ if response.status_code != 200:
1232
+ self._reset_update_button()
1233
+ QMessageBox.warning(self, "获取失败", f"无法获取远程版本信息: {response.status_code}")
1234
+ return
1235
+
1236
+ remote_version = response.text.strip()
1237
+
1238
+ # 读取本地version.txt
1239
+ try:
1240
+ if self.project_path:
1241
+ version_file = os.path.join(self.project_path, "version.txt")
1242
+ else:
1243
+ version_file = "version.txt"
1244
+
1245
+ with open(version_file, "r", encoding="utf-8") as f:
1246
+ local_version = f.read().strip()
1247
+ except:
1248
+ local_version = "1.0.0"
1249
+
1250
+ self._reset_update_button()
1251
+
1252
+ # 比较版本 - 使用版本号解析比较
1253
+ if self._version_compare(remote_version, local_version) > 0:
1254
+ # 检查是否有更新对话框可用
1255
+ if UpdateInfoDialog:
1256
+ # 显示详细的更新信息对话框
1257
+ update_dialog = UpdateInfoDialog(local_version, remote_version, self.project_path, self)
1258
+ if update_dialog.exec() == QDialog.Accepted and update_dialog.should_update:
1259
+ # 用户确认更新,继续执行git pull
1260
+ pass
1261
+ else:
1262
+ return # 用户取消更新
1263
+ else:
1264
+ # 回退到原有的简单对话框
1265
+ reply = QMessageBox.question(
1266
+ self, "发现更新",
1267
+ f"本地版本: {local_version}\n远程版本: {remote_version}\n\n是否立即更新?",
1268
+ QMessageBox.Yes | QMessageBox.No
1269
+ )
1270
+ if reply != QMessageBox.Yes:
1271
+ return
1272
+ # 执行git pull - 在server.py脚本所在目录执行
1273
+ try:
1274
+ # 获取server.py脚本所在的目录
1275
+ server_dir = os.path.dirname(os.path.abspath(__file__))
1276
+
1277
+ result = subprocess.run(
1278
+ ["git", "pull"],
1279
+ capture_output=True,
1280
+ text=True,
1281
+ cwd=server_dir,
1282
+ timeout=30
1283
+ )
1284
+ if result.returncode == 0:
1285
+ QMessageBox.information(self, "更新成功", "代码已更新到最新版本")
1286
+ else:
1287
+ QMessageBox.critical(self, "更新失败", f"git pull失败:\n{result.stderr}")
1288
+ except subprocess.TimeoutExpired:
1289
+ QMessageBox.critical(self, "更新失败", "git pull超时")
1290
+ except Exception as e:
1291
+ QMessageBox.critical(self, "更新失败", f"执行git pull失败: {e}")
1292
+ else:
1293
+ QMessageBox.information(self, "已是最新", "当前已是最新版本")
1294
+
1295
+ except requests.RequestException as e:
1296
+ self._reset_update_button()
1297
+ QMessageBox.critical(self, "网络错误", f"检查更新失败: {e}")
1298
+ except Exception as e:
1299
+ self._reset_update_button()
1300
+ QMessageBox.critical(self, "检查失败", f"检查更新失败: {e}")
1301
+
1302
+ def _reset_update_button(self):
1303
+ """重置更新按钮状态"""
1304
+ self.update_button.setEnabled(True)
1305
+ self.update_button.setText("检查更新")
1306
+
1307
+ def _version_compare(self, version1: str, version2: str) -> int:
1308
+ """
1309
+ 比较两个版本号
1310
+
1311
+ Args:
1312
+ version1: 第一个版本号
1313
+ version2: 第二个版本号
1314
+
1315
+ Returns:
1316
+ int: 1 if version1 > version2, -1 if version1 < version2, 0 if equal
1317
+ """
1318
+ try:
1319
+ # 解析版本号为整数列表
1320
+ v1_parts = [int(x) for x in version1.split('.')]
1321
+ v2_parts = [int(x) for x in version2.split('.')]
1322
+
1323
+ # 补齐较短的版本号(比如 1.0 补齐为 1.0.0)
1324
+ max_length = max(len(v1_parts), len(v2_parts))
1325
+ v1_parts.extend([0] * (max_length - len(v1_parts)))
1326
+ v2_parts.extend([0] * (max_length - len(v2_parts)))
1327
+
1328
+ # 逐位比较
1329
+ for v1, v2 in zip(v1_parts, v2_parts):
1330
+ if v1 > v2:
1331
+ return 1
1332
+ elif v1 < v2:
1333
+ return -1
1334
+
1335
+ return 0 # 版本号相等
1336
+
1337
+ except ValueError:
1338
+ # 如果无法解析版本号,回退到字符串比较
1339
+ if version1 > version2:
1340
+ return 1
1341
+ elif version1 < version2:
1342
+ return -1
1343
+ else:
1344
+ return 0
1345
+
1346
+ def _set_smart_position(self):
1347
+ """设置智能窗口位置,避免多窗口重叠"""
1348
+ if WindowPositionManager:
1349
+ try:
1350
+ # 获取下一个窗口位置
1351
+ x, y = WindowPositionManager.get_next_position('main')
1352
+ self.move(x, y)
1353
+ # 保存当前位置供后续清理
1354
+ self._window_position = (x, y)
1355
+ except Exception as e:
1356
+ print(f"设置窗口位置失败: {e}")
1357
+ # 如果失败,使用默认居中
1358
+ self._center_window()
1359
+ else:
1360
+ # 没有位置管理器时,使用默认居中
1361
+ self._center_window()
1362
+
1363
+ def _center_window(self):
1364
+ """将窗口居中显示"""
1365
+ from PySide6.QtGui import QGuiApplication
1366
+ screen = QGuiApplication.primaryScreen()
1367
+ if screen:
1368
+ screen_geometry = screen.availableGeometry()
1369
+ x = screen_geometry.x() + (screen_geometry.width() - self.width()) // 2
1370
+ y = screen_geometry.y() + (screen_geometry.height() - self.height()) // 2
1371
+ self.move(x, y)
1372
+
1373
+
1374
+ def _update_countdown(self):
1375
+ """更新倒计时 - 优化版:增强错误处理,避免加密环境下的异常"""
1376
+ try:
1377
+ self.elapsed_time += 1
1378
+
1379
+ # 更新聊天标签页的进度条(增加安全检查)
1380
+ if self.chat_tab and hasattr(self.chat_tab, 'update_progress'):
1381
+ try:
1382
+ self.chat_tab.update_progress(self.elapsed_time)
1383
+ except Exception as e:
1384
+ logger = get_debug_logger()
1385
+ logger.log_warning(f"Failed to update chat progress: {e}", "UI")
1386
+
1387
+ # 检查是否超时
1388
+ if self.elapsed_time >= self.timeout:
1389
+ self.countdown_timer.stop()
1390
+ # 超时前检查输入框是否有内容,如果有则保存到历史记录
1391
+ if self.chat_tab and hasattr(self.chat_tab, 'save_input_to_history'):
1392
+ try:
1393
+ self.chat_tab.save_input_to_history()
1394
+ except Exception as e:
1395
+ logger = get_debug_logger()
1396
+ logger.log_warning(f"Failed to save input to history on timeout: {e}", "UI")
1397
+ # 自动提交空反馈
1398
+ self._handle_feedback_submitted([], [])
1399
+ except Exception as e:
1400
+ logger = get_debug_logger()
1401
+ logger.log_error(f"倒计时更新失败: {e}", "UI")
1402
+ # 确保定时器停止,避免无限循环
1403
+ if self.countdown_timer.isActive():
1404
+ self.countdown_timer.stop()
1405
+
1406
+
1407
+
1408
+ def _temp_close(self):
1409
+ """临时关闭(精简版按钮),不写入结果"""
1410
+ self.is_temp_close = True
1411
+ self.close()
1412
+
1413
+ def closeEvent(self, event):
1414
+ """关闭事件处理"""
1415
+ # 清理窗口位置记录
1416
+ if WindowPositionManager and hasattr(self, '_window_position'):
1417
+ try:
1418
+ x, y = self._window_position
1419
+ WindowPositionManager.remove_position('main', x, y)
1420
+ except Exception:
1421
+ pass # 静默处理错误
1422
+
1423
+ # 停止并等待版本检查线程结束
1424
+ try:
1425
+ if self.version_check_thread is not None:
1426
+ if self.version_check_thread.isRunning():
1427
+ self.version_check_thread.request_stop()
1428
+ # 等待线程结束,超时后强制终止
1429
+ if not self.version_check_thread.wait(3000): # 等待3秒
1430
+ # 超时,强制终止线程
1431
+ self.version_check_thread.terminate()
1432
+ self.version_check_thread.wait(1000) # 等待终止完成
1433
+ # 清理引用
1434
+ self.version_check_thread = None
1435
+ except (RuntimeError, AttributeError):
1436
+ pass # 对象可能已被删除
1437
+
1438
+ # 停止定时器
1439
+ if self.countdown_timer.isActive():
1440
+ self.countdown_timer.stop()
1441
+
1442
+ # 停止ESC定时器
1443
+ if hasattr(self, 'esc_timer') and self.esc_timer.isActive():
1444
+ self.esc_timer.stop()
1445
+
1446
+ # 清理 chat_tab 中的弹窗等子控件,避免 Qt 对象销毁顺序问题
1447
+ if self.chat_tab:
1448
+ try:
1449
+ # 清理输入框的指令弹窗和文件弹窗
1450
+ if hasattr(self.chat_tab, 'input_text'):
1451
+ input_text = self.chat_tab.input_text
1452
+ if hasattr(input_text, '_close_command_popup'):
1453
+ input_text._close_command_popup()
1454
+ if hasattr(input_text, '_close_file_popup'):
1455
+ input_text._close_file_popup()
1456
+ except Exception:
1457
+ pass # 静默处理错误
1458
+
1459
+ # 处理延迟删除队列,确保 deleteLater 的对象被正确删除
1460
+ try:
1461
+ QApplication.processEvents()
1462
+ except Exception:
1463
+ pass
1464
+
1465
+ # 在关闭前保存输入框内容到历史记录(无论是超时还是用户主动关闭)
1466
+ if self.chat_tab and hasattr(self.chat_tab, 'save_input_to_history'):
1467
+ try:
1468
+ self.chat_tab.save_input_to_history()
1469
+ except Exception as e:
1470
+ logger = get_debug_logger()
1471
+ logger.log_warning(f"Failed to save input to history on close: {e}", "UI")
1472
+
1473
+ # 临时关闭(精简版按钮):不写入结果,直接关闭
1474
+ if self.is_temp_close:
1475
+ event.accept()
1476
+ return
1477
+
1478
+ # 如果没有反馈结果(说明是用户主动关闭,而不是正常提交或超时),设置特定的反馈结果
1479
+ if not self.feedback_result:
1480
+ # 区分关闭方式
1481
+ if self.session_id:
1482
+ try:
1483
+ manager = SessionManager(session_id=self.session_id, project_path=self.project_path)
1484
+
1485
+ # 判断是超时关闭还是用户点击关闭
1486
+ if self.elapsed_time >= self.timeout:
1487
+ # 超时自动关闭
1488
+ manager.mark_timeout_closed(self.session_id)
1489
+ else:
1490
+ # 用户主动关闭(点击关闭按钮或快捷键)
1491
+ manager.mark_user_closed_by_button(self.session_id)
1492
+ except Exception as e:
1493
+ logger = get_debug_logger()
1494
+ logger.log_warning(f"Failed to mark session close type: {e}", "UI")
1495
+
1496
+ self.feedback_result = {
1497
+ 'content': [{"type": "text", "text": "STOP!请立即停止任何工作,不要再调用任何工具、回复任何消息。STOP!"}],
1498
+ 'images': []
1499
+ }
1500
+
1501
+ # 保存设置
1502
+ settings = QSettings("FeedbackUI", "MainWindow")
1503
+ settings.setValue("geometry", self.saveGeometry())
1504
+ settings.setValue("state", self.saveState())
1505
+
1506
+ event.accept()
1507
+
1508
+ def _setup_shortcuts(self):
1509
+ """设置快捷键"""
1510
+ from PySide6.QtGui import QShortcut, QKeySequence
1511
+
1512
+ # Cmd+W 或 Ctrl+W 关闭窗口
1513
+ close_shortcut = QShortcut(QKeySequence("Ctrl+W"), self)
1514
+ close_shortcut.activated.connect(self._handle_close_shortcut)
1515
+
1516
+ # macOS 上的 Cmd+W
1517
+ if sys.platform == "darwin":
1518
+ cmd_close_shortcut = QShortcut(QKeySequence("Meta+W"), self)
1519
+ cmd_close_shortcut.activated.connect(self._handle_close_shortcut)
1520
+
1521
+ def _handle_close_shortcut(self):
1522
+ """处理关闭快捷键"""
1523
+ # 直接关闭窗口,让closeEvent处理统一逻辑
1524
+ self.close()
1525
+
1526
+ def keyPressEvent(self, event):
1527
+ """处理按键事件"""
1528
+ from PySide6.QtCore import Qt
1529
+
1530
+ # 检测双击ESC
1531
+ if event.key() == Qt.Key_Escape:
1532
+ self.esc_press_count += 1
1533
+
1534
+ if self.esc_press_count == 1:
1535
+ # 第一次按ESC,启动计时器(500ms内需要再按一次)
1536
+ self.esc_timer.start(500)
1537
+ elif self.esc_press_count == 2:
1538
+ # 第二次按ESC,关闭窗口
1539
+ self.esc_timer.stop()
1540
+ self.esc_press_count = 0
1541
+
1542
+ # 直接关闭窗口,让closeEvent处理统一逻辑
1543
+ self.close()
1544
+ return # 避免事件继续传播
1545
+
1546
+ # 调用父类处理
1547
+ super().keyPressEvent(event)
1548
+
1549
+ def _reset_esc_count(self):
1550
+ """重置ESC计数器"""
1551
+ self.esc_press_count = 0
1552
+
1553
+ def run(self) -> FeedbackResult:
1554
+ """运行反馈界面并返回结果"""
1555
+ # 确保窗口显示在最前面
1556
+ self.show()
1557
+ self.raise_() # 把窗口提到前台
1558
+ self.activateWindow() # 激活窗口
1559
+
1560
+ # 在macOS上确保窗口获得焦点
1561
+ import platform
1562
+ if platform.system() == 'Darwin': # macOS
1563
+ self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
1564
+
1565
+ # 启动事件循环
1566
+ app = QApplication.instance()
1567
+ app.exec()
1568
+
1569
+ # 临时关闭时返回None,不写入结果
1570
+ if self.is_temp_close:
1571
+ return None
1572
+ return self.feedback_result or {"content": [], "images": []}
1573
+
1574
+
1575
+ def feedback_ui(prompt: str, predefined_options: Optional[List[str]] = None, output_file: Optional[str] = None, project_path: Optional[str] = None, work_title: Optional[str] = None, timeout: int = 60, skip_init_check: bool = False, session_id: Optional[str] = None, workspace_id: Optional[str] = None, files: Optional[List[str]] = None, bugdetail: Optional[str] = None, ide: Optional[str] = None) -> Optional[FeedbackResult]:
1576
+ """
1577
+ 创建并显示反馈UI界面
1578
+
1579
+ Args:
1580
+ prompt: 显示给用户的提示信息
1581
+ predefined_options: 预定义的选项列表
1582
+ output_file: 输出文件路径(暂未使用)
1583
+ project_path: 项目路径
1584
+ timeout: 超时时间(秒)
1585
+
1586
+ Returns:
1587
+ FeedbackResult: 包含用户反馈和图片的结果
1588
+ """
1589
+ # 首先确保有QApplication实例 - 这在PyArmor加密环境中非常重要
1590
+ app = QApplication.instance()
1591
+ if app is None:
1592
+ app = QApplication(sys.argv)
1593
+ # 设置应用程序退出策略,避免在加密环境中出现问题
1594
+ app.setQuitOnLastWindowClosed(True)
1595
+
1596
+ # 设置暗色主题
1597
+ try:
1598
+ app.setPalette(get_dark_mode_palette(app))
1599
+ app.setStyle("Fusion") # 与原版保持一致:设置Fusion样式
1600
+ except Exception as e:
1601
+ logger = get_debug_logger()
1602
+ logger.log_warning(f"主题设置失败: {e}", "UI")
1603
+
1604
+ # 创建反馈UI(现在QApplication已经存在)
1605
+ try:
1606
+ ui = FeedbackUI(prompt, predefined_options, project_path, work_title, timeout, skip_auth_check=False, skip_init_check=skip_init_check, session_id=session_id, workspace_id=workspace_id, files=files, bugdetail=bugdetail, ide=ide) # 恢复认证检查
1607
+ except Exception as e:
1608
+ logger = get_debug_logger()
1609
+ logger.log_error(f"FeedbackUI创建失败: {e}", "UI")
1610
+ import traceback
1611
+ traceback.print_exc()
1612
+ return {"content": [], "images": []}
1613
+
1614
+ # 运行并获取结果
1615
+ try:
1616
+ result = ui.run()
1617
+ return result
1618
+ except Exception as e:
1619
+ logger = get_debug_logger()
1620
+ logger.log_error(f"UI运行失败: {e}", "UI")
1621
+ import traceback
1622
+ traceback.print_exc()
1623
+ return {"content": [], "images": []}
1624
+
1625
+
1626
+ if __name__ == "__main__":
1627
+ import argparse
1628
+ import pickle
1629
+
1630
+ parser = argparse.ArgumentParser(description='Feedback UI')
1631
+ parser.add_argument('--prompt', required=True, help='显示给用户的提示信息')
1632
+ parser.add_argument('--predefined-options', help='预定义选项(用|||分隔)')
1633
+ parser.add_argument('--project-path', help='项目路径')
1634
+ parser.add_argument('--work-title', help='当前工作标题')
1635
+ parser.add_argument('--timeout', type=int, default=60, help='超时时间(秒)')
1636
+ parser.add_argument('--skip-init-check', action='store_true', help='跳过项目初始化检查')
1637
+ parser.add_argument('--session-id', help='Claude Code会话ID')
1638
+ parser.add_argument('--workspace-id', help='工作空间ID')
1639
+ parser.add_argument('--files', help='AI创建或修改的文件路径(用|||分隔)')
1640
+ parser.add_argument('--bugdetail', help='正在修复的bug简介')
1641
+ parser.add_argument('--ide', help='指定使用的IDE(例如:cursor/vscode/kiro/qoder等)')
1642
+ parser.add_argument('--output-file', help='输出文件路径')
1643
+
1644
+ args = parser.parse_args()
1645
+
1646
+ # 解析预定义选项
1647
+ predefined_options = None
1648
+ if args.predefined_options:
1649
+ predefined_options = args.predefined_options.split('|||')
1650
+
1651
+ # 解析文件列表
1652
+ files = None
1653
+ if args.files:
1654
+ files = args.files.split('|||')
1655
+
1656
+ # 调用反馈UI
1657
+ result = feedback_ui(
1658
+ prompt=args.prompt,
1659
+ predefined_options=predefined_options,
1660
+ project_path=args.project_path,
1661
+ work_title=args.work_title,
1662
+ timeout=args.timeout,
1663
+ skip_init_check=args.skip_init_check,
1664
+ session_id=args.session_id,
1665
+ workspace_id=args.workspace_id,
1666
+ files=files,
1667
+ bugdetail=args.bugdetail,
1668
+ ide=args.ide
1669
+ )
1670
+
1671
+ # 如果指定了输出文件且有结果,写入文件
1672
+ if args.output_file and result is not None:
1673
+ try:
1674
+ with open(args.output_file, 'wb') as f:
1675
+ pickle.dump(result, f)
1676
+ except Exception as e:
1677
+ print(f"写入输出文件失败: {e}", file=sys.stderr)
1678
+ sys.exit(1)
1679
+ elif result is not None:
1680
+ print(f"结果: {result}")