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.

@@ -0,0 +1,187 @@
1
+ """
2
+ Agent 内容弹窗组件
3
+ """
4
+
5
+ from typing import Dict
6
+ from PySide6.QtWidgets import (
7
+ QFrame, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QApplication
8
+ )
9
+ from PySide6.QtCore import Qt, Signal, QPoint
10
+ from PySide6.QtGui import QKeyEvent
11
+
12
+ from .markdown_display import MarkdownDisplayWidget
13
+
14
+
15
+ class AgentPopup(QFrame):
16
+ """Agent 内容弹窗组件"""
17
+
18
+ popup_closed = Signal()
19
+
20
+ def __init__(self, parent=None):
21
+ super().__init__(parent)
22
+ self._setup_ui()
23
+ self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
24
+ self.setAttribute(Qt.WA_DeleteOnClose)
25
+
26
+ def _setup_ui(self):
27
+ """设置UI"""
28
+ layout = QVBoxLayout(self)
29
+ layout.setContentsMargins(0, 0, 0, 0)
30
+ layout.setSpacing(0)
31
+
32
+ # 标题栏
33
+ title_frame = QFrame()
34
+ title_frame.setStyleSheet("""
35
+ QFrame {
36
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
37
+ border-top-left-radius: 8px;
38
+ border-top-right-radius: 8px;
39
+ }
40
+ """)
41
+ title_layout = QHBoxLayout(title_frame)
42
+ title_layout.setContentsMargins(12, 8, 12, 8)
43
+
44
+ self.title_label = QLabel()
45
+ self.title_label.setStyleSheet("""
46
+ QLabel {
47
+ color: white;
48
+ font-size: 14px;
49
+ font-weight: bold;
50
+ }
51
+ """)
52
+ title_layout.addWidget(self.title_label)
53
+
54
+ close_button = QPushButton("✕")
55
+ close_button.setFixedSize(20, 20)
56
+ close_button.setStyleSheet("""
57
+ QPushButton {
58
+ background: transparent;
59
+ color: white;
60
+ border: none;
61
+ font-size: 14px;
62
+ font-weight: bold;
63
+ }
64
+ QPushButton:hover {
65
+ background: rgba(255, 255, 255, 0.2);
66
+ border-radius: 10px;
67
+ }
68
+ """)
69
+ close_button.clicked.connect(self.close)
70
+ title_layout.addWidget(close_button)
71
+
72
+ layout.addWidget(title_frame)
73
+
74
+ # 内容区域
75
+ self.content_widget = MarkdownDisplayWidget()
76
+ self.content_widget.setStyleSheet("""
77
+ QTextEdit {
78
+ background: #1e1e1e;
79
+ border: none;
80
+ padding: 12px;
81
+ color: #e0e0e0;
82
+ font-size: 13px;
83
+ }
84
+ QScrollBar:vertical {
85
+ background: #2b2b2b;
86
+ width: 8px;
87
+ border-radius: 4px;
88
+ }
89
+ QScrollBar::handle:vertical {
90
+ background: #555;
91
+ border-radius: 4px;
92
+ min-height: 30px;
93
+ }
94
+ QScrollBar::handle:vertical:hover {
95
+ background: #666;
96
+ }
97
+ """)
98
+ layout.addWidget(self.content_widget, 1)
99
+
100
+ # 底部统计栏
101
+ stats_frame = QFrame()
102
+ stats_frame.setStyleSheet("""
103
+ QFrame {
104
+ background: #2b2b2b;
105
+ border-bottom-left-radius: 8px;
106
+ border-bottom-right-radius: 8px;
107
+ padding: 8px;
108
+ }
109
+ """)
110
+ stats_layout = QHBoxLayout(stats_frame)
111
+
112
+ self.stats_label = QLabel()
113
+ self.stats_label.setStyleSheet("""
114
+ QLabel {
115
+ color: #888;
116
+ font-size: 11px;
117
+ }
118
+ """)
119
+ stats_layout.addWidget(self.stats_label, alignment=Qt.AlignCenter)
120
+
121
+ layout.addWidget(stats_frame)
122
+
123
+ # 整体样式
124
+ self.setStyleSheet("""
125
+ AgentPopup {
126
+ background: #1e1e1e;
127
+ border: 1px solid #444;
128
+ border-radius: 8px;
129
+ }
130
+ """)
131
+
132
+ def set_agent_data(self, agent_record: Dict):
133
+ """设置 agent 数据"""
134
+ subagent_type = agent_record.get('subagent_type', 'Agent')
135
+ description = agent_record.get('description', '')
136
+ content = agent_record.get('content', '')
137
+ duration_ms = agent_record.get('duration_ms', 0)
138
+ tokens = agent_record.get('tokens', 0)
139
+ tool_calls = agent_record.get('tool_calls', 0)
140
+
141
+ # 设置标题
142
+ self.title_label.setText(f"{subagent_type}: {description}")
143
+
144
+ # 设置内容
145
+ self.content_widget.setMarkdownText(content)
146
+
147
+ # 设置统计信息
148
+ stats_text = f"耗时: {duration_ms}ms | Tokens: {tokens} | 工具调用: {tool_calls}次"
149
+ self.stats_label.setText(stats_text)
150
+
151
+ def show_at_position(self, position: QPoint):
152
+ """在指定位置显示弹窗"""
153
+ screen = QApplication.primaryScreen()
154
+ screen_geometry = screen.availableGeometry()
155
+
156
+ popup_width = 500
157
+ popup_height = 700
158
+
159
+ self.resize(popup_width, popup_height)
160
+
161
+ if position and position.x() >= 0 and position.y() >= 0:
162
+ x = position.x()
163
+ y = position.y()
164
+
165
+ if x + popup_width > screen_geometry.right():
166
+ x = screen_geometry.right() - popup_width - 10
167
+ if y + popup_height > screen_geometry.bottom():
168
+ y = screen_geometry.bottom() - popup_height - 10
169
+ if x < screen_geometry.x():
170
+ x = screen_geometry.x()
171
+ if y < screen_geometry.y():
172
+ y = screen_geometry.y()
173
+ else:
174
+ x = screen_geometry.x() + (screen_geometry.width() - popup_width) // 2
175
+ y = screen_geometry.y() + (screen_geometry.height() - popup_height) // 2
176
+
177
+ self.move(x, y)
178
+ self.show()
179
+ self.setFocus()
180
+
181
+ def keyPressEvent(self, event: QKeyEvent):
182
+ """处理键盘事件"""
183
+ if event.key() == Qt.Key_Escape:
184
+ self.popup_closed.emit()
185
+ self.close()
186
+ else:
187
+ super().keyPressEvent(event)
@@ -0,0 +1,281 @@
1
+ """
2
+ 聊天历史记录管理模块
3
+
4
+ 功能:
5
+ 1. 保存用户输入的聊天内容
6
+ 2. 加载和显示历史记录
7
+ 3. 提供弹窗方式显示历史记录
8
+ 4. 支持插入和复制功能
9
+ """
10
+
11
+ import os
12
+ import json
13
+ from datetime import datetime
14
+ from typing import List, Dict, Optional
15
+ from PySide6.QtWidgets import QApplication, QMessageBox
16
+ from PySide6.QtCore import QPoint
17
+
18
+ # 导入历史记录弹窗组件
19
+ try:
20
+ from .history_popup import HistoryPopup
21
+ HISTORY_POPUP_AVAILABLE = True
22
+ except ImportError:
23
+ try:
24
+ from history_popup import HistoryPopup
25
+ HISTORY_POPUP_AVAILABLE = True
26
+ except ImportError:
27
+ HISTORY_POPUP_AVAILABLE = False
28
+ print("Warning: HistoryPopup component not available")
29
+
30
+
31
+ class ChatHistoryManager:
32
+ """聊天历史记录管理器"""
33
+
34
+ def __init__(self, project_path: Optional[str] = None, session_id: Optional[str] = None):
35
+ self.project_path = project_path
36
+ self.session_id = session_id
37
+
38
+ def get_history_file_path(self) -> Optional[str]:
39
+ """获取历史记录文件路径"""
40
+ # 如果没有session_id,返回None
41
+ if not self.session_id:
42
+ return None
43
+
44
+ if self.project_path:
45
+ return os.path.join(self.project_path, '.workspace', 'chat_history', f'{self.session_id}.json')
46
+ else:
47
+ # 如果没有项目路径,使用脚本目录
48
+ script_dir = os.path.dirname(os.path.abspath(__file__))
49
+ return os.path.join(script_dir, '..', '.workspace', 'chat_history', f'{self.session_id}.json')
50
+
51
+ def save_to_history(self, content: str) -> bool:
52
+ """保存内容到历史记录
53
+
54
+ Args:
55
+ content: 要保存的内容
56
+
57
+ Returns:
58
+ bool: 保存是否成功
59
+ """
60
+ if not content.strip():
61
+ return False
62
+
63
+ try:
64
+ # 获取历史记录文件路径
65
+ history_file = self.get_history_file_path()
66
+
67
+ # 如果没有session_id,静默跳过
68
+ if not history_file:
69
+ return False
70
+
71
+ # 读取完整的文件数据
72
+ file_data = {}
73
+ if os.path.exists(history_file):
74
+ with open(history_file, 'r', encoding='utf-8') as f:
75
+ data = json.load(f)
76
+
77
+ # 新格式:{'dialogues': [...], 'control': {...}}
78
+ if isinstance(data, dict) and 'dialogues' in data:
79
+ file_data = data
80
+ # 旧格式数组,转换为新格式
81
+ elif isinstance(data, list):
82
+ file_data = {
83
+ 'dialogues': [record for record in data if isinstance(record, dict) and record.get('type') != 'stop_hook_status']
84
+ }
85
+ else:
86
+ file_data = {'dialogues': []}
87
+ else:
88
+ # 文件不存在,初始化为新格式
89
+ file_data = {'dialogues': []}
90
+
91
+ # 添加新记录到dialogues数组
92
+ new_record = {
93
+ 'content': content.strip(),
94
+ 'timestamp': datetime.now().isoformat(),
95
+ 'time_display': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
96
+ }
97
+
98
+ # 确保dialogues字段存在
99
+ if 'dialogues' not in file_data:
100
+ file_data['dialogues'] = []
101
+
102
+ file_data['dialogues'].append(new_record)
103
+
104
+ # 保存完整的文件数据
105
+ os.makedirs(os.path.dirname(history_file), exist_ok=True)
106
+ with open(history_file, 'w', encoding='utf-8') as f:
107
+ json.dump(file_data, f, ensure_ascii=False, indent=2)
108
+
109
+ return True
110
+
111
+ except Exception as e:
112
+ print(f"保存历史记录失败: {e}")
113
+ return False
114
+
115
+ def load_history_from_file(self) -> List[Dict]:
116
+ """从文件加载历史记录(兼容旧格式和新格式)"""
117
+ try:
118
+ history_file = self.get_history_file_path()
119
+
120
+ # 如果没有session_id,返回空列表
121
+ if not history_file:
122
+ return []
123
+
124
+ if os.path.exists(history_file):
125
+ with open(history_file, 'r', encoding='utf-8') as f:
126
+ data = json.load(f)
127
+
128
+ # 新格式:{'dialogues': [...], 'control': {...}}
129
+ if isinstance(data, dict) and 'dialogues' in data:
130
+ # 返回dialogues数组
131
+ return data.get('dialogues', [])
132
+
133
+ # 旧格式数组
134
+ if isinstance(data, list):
135
+ # 过滤掉control记录,保留对话记录
136
+ return [record for record in data if isinstance(record, dict) and record.get('type') != 'stop_hook_status']
137
+
138
+ return []
139
+ return []
140
+ except Exception as e:
141
+ print(f"加载历史记录失败: {e}")
142
+ return []
143
+
144
+ def get_recent_history(self, count: Optional[int] = None) -> List[Dict]:
145
+ """获取最近的历史记录
146
+
147
+ Args:
148
+ count: 获取记录数量,如果为None则返回所有记录
149
+
150
+ Returns:
151
+ List[Dict]: 历史记录列表
152
+ """
153
+ history = self.load_history_from_file()
154
+ if count is None:
155
+ return history # 返回所有历史记录
156
+ return history[-count:]
157
+
158
+ def get_agent_records_after_last_user(self) -> List[Dict]:
159
+ """获取最后一条用户消息之后的所有 agent 记录"""
160
+ history = self.load_history_from_file()
161
+
162
+ # 找到最后一条用户消息的索引
163
+ last_user_idx = -1
164
+ for i, record in enumerate(history):
165
+ if record.get('role') == 'user' or 'messages' in record:
166
+ last_user_idx = i
167
+
168
+ # 返回之后的所有 agent 记录
169
+ if last_user_idx >= 0:
170
+ return [r for r in history[last_user_idx + 1:] if r.get('role') == 'agent']
171
+
172
+ # 如果没有用户消息,返回所有 agent 记录
173
+ return [r for r in history if r.get('role') == 'agent']
174
+
175
+ def _get_draft_file_path(self) -> Optional[str]:
176
+ """获取草稿文件路径(独立文件,避免被其他代码覆盖)"""
177
+ if not self.project_path or not self.session_id:
178
+ return None
179
+ return os.path.join(self.project_path, '.workspace', 'chat_history', f'{self.session_id}_draft.json')
180
+
181
+ def get_latest_draft(self) -> Optional[dict]:
182
+ """获取最近的未提交草稿"""
183
+ try:
184
+ draft_file = self._get_draft_file_path()
185
+ if not draft_file or not os.path.exists(draft_file):
186
+ return None
187
+
188
+ with open(draft_file, 'r', encoding='utf-8') as f:
189
+ return json.load(f)
190
+ except Exception:
191
+ return None
192
+
193
+ def save_draft(self, text: str) -> bool:
194
+ """保存草稿到独立文件"""
195
+ try:
196
+ draft_file = self._get_draft_file_path()
197
+ if not draft_file:
198
+ return False
199
+
200
+ os.makedirs(os.path.dirname(draft_file), exist_ok=True)
201
+ with open(draft_file, 'w', encoding='utf-8') as f:
202
+ json.dump({'text': text}, f, ensure_ascii=False)
203
+
204
+ return True
205
+ except Exception:
206
+ return False
207
+
208
+ def clear_draft(self) -> bool:
209
+ """删除草稿文件"""
210
+ try:
211
+ draft_file = self._get_draft_file_path()
212
+ if draft_file and os.path.exists(draft_file):
213
+ os.remove(draft_file)
214
+ return True
215
+ except Exception:
216
+ return False
217
+
218
+ def show_history_dialog(self, parent=None) -> None:
219
+ """显示历史记录弹窗
220
+
221
+ Args:
222
+ parent: 父窗口
223
+ """
224
+ try:
225
+ # 加载历史记录
226
+ history = self.get_recent_history()
227
+
228
+ if not history:
229
+ QMessageBox.information(parent, "历史记录", "暂无历史记录")
230
+ return
231
+
232
+ # 检查弹窗组件是否可用
233
+ if not HISTORY_POPUP_AVAILABLE:
234
+ QMessageBox.critical(parent, "错误", "历史记录弹窗组件不可用")
235
+ return
236
+
237
+ # 显示历史记录弹窗
238
+ self._show_history_popup(parent, history)
239
+
240
+ except Exception as e:
241
+ print(f"显示历史记录失败: {e}")
242
+ QMessageBox.critical(parent, "错误", f"显示历史记录失败: {str(e)}")
243
+
244
+ def _show_history_popup(self, parent, history: List[Dict]):
245
+ """显示历史记录弹窗"""
246
+ try:
247
+ # 创建弹窗
248
+ popup = HistoryPopup(parent)
249
+
250
+ # 设置历史记录数据
251
+ popup.set_history_records(history, parent)
252
+
253
+ # 连接信号
254
+ popup.content_inserted.connect(lambda content: print(f"✅ 内容已插入: {content[:50]}..."))
255
+ popup.content_copied.connect(lambda content: print(f"✅ 内容已复制: {content[:50]}..."))
256
+ popup.popup_closed.connect(lambda: print("📝 历史记录弹窗已关闭"))
257
+
258
+ # 计算弹窗位置(在历史按钮附近)
259
+ if hasattr(parent, 'history_button'):
260
+ button = parent.history_button
261
+ # 获取按钮的全局位置
262
+ button_pos = button.mapToGlobal(button.rect().bottomLeft())
263
+ # 稍微偏移一下位置
264
+ popup_pos = QPoint(button_pos.x() - 200, button_pos.y() + 5)
265
+ else:
266
+ # 如果找不到按钮,在父窗口中央显示
267
+ if parent:
268
+ parent_rect = parent.geometry()
269
+ popup_pos = QPoint(
270
+ parent_rect.x() + parent_rect.width() // 2 - 250,
271
+ parent_rect.y() + parent_rect.height() // 2 - 200
272
+ )
273
+ else:
274
+ popup_pos = QPoint(100, 100)
275
+
276
+ # 显示弹窗
277
+ popup.show_at_position(popup_pos)
278
+
279
+ except Exception as e:
280
+ print(f"显示历史记录弹窗失败: {e}")
281
+ QMessageBox.critical(parent, "错误", f"显示历史记录弹窗失败: {str(e)}")