feedback-mcp 1.0.64__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of feedback-mcp might be problematic. Click here for more details.
- add_command_dialog.py +712 -0
- command.py +636 -0
- components/__init__.py +15 -0
- components/agent_popup.py +187 -0
- components/chat_history.py +281 -0
- components/command_popup.py +399 -0
- components/feedback_text_edit.py +1125 -0
- components/file_popup.py +417 -0
- components/history_popup.py +582 -0
- components/markdown_display.py +262 -0
- context_formatter.py +301 -0
- debug_logger.py +107 -0
- feedback_config.py +144 -0
- feedback_mcp-1.0.64.dist-info/METADATA +327 -0
- feedback_mcp-1.0.64.dist-info/RECORD +41 -0
- feedback_mcp-1.0.64.dist-info/WHEEL +5 -0
- feedback_mcp-1.0.64.dist-info/entry_points.txt +2 -0
- feedback_mcp-1.0.64.dist-info/top_level.txt +20 -0
- feedback_ui.py +1680 -0
- get_session_id.py +53 -0
- git_operations.py +579 -0
- ide_utils.py +313 -0
- path_config.py +89 -0
- post_task_hook.py +78 -0
- record.py +188 -0
- server.py +746 -0
- session_manager.py +368 -0
- stop_hook.py +87 -0
- tabs/__init__.py +87 -0
- tabs/base_tab.py +34 -0
- tabs/chat_history_style.qss +66 -0
- tabs/chat_history_tab.py +1000 -0
- tabs/chat_tab.py +1931 -0
- tabs/workspace_tab.py +502 -0
- ui/__init__.py +20 -0
- ui/__main__.py +16 -0
- ui/compact_feedback_ui.py +376 -0
- ui/session_list_ui.py +793 -0
- ui/styles/session_list.qss +158 -0
- window_position_manager.py +197 -0
- workspace_manager.py +253 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Session List UI Styles
|
|
3
|
+
* Theme: Modern Dark / macOS Notification Center Style
|
|
4
|
+
*
|
|
5
|
+
* REQUIRED: Ensure the following ObjectNames are set in your Python code:
|
|
6
|
+
* - Main Container: "mainContainer"
|
|
7
|
+
* - Title Bar: "titleBar"
|
|
8
|
+
* - Title Label: "titleLabel"
|
|
9
|
+
* - Collapse Button: "collapseButton"
|
|
10
|
+
* - Scroll Area: "scrollArea"
|
|
11
|
+
* - Session Item Widget: "sessionCard"
|
|
12
|
+
* - Project Label: "projectLabel"
|
|
13
|
+
* - Workspace Label: "workspaceLabel"
|
|
14
|
+
* - Stage Label: "stageLabel"
|
|
15
|
+
* - Conversation Label: "conversationLabel"
|
|
16
|
+
* - Progress Bar: "progressBar"
|
|
17
|
+
* - Time Label: "timeLabel"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/* Main Container */
|
|
21
|
+
QWidget#mainContainer {
|
|
22
|
+
background-color: rgba(35, 35, 35, 240); /* Slightly transparent dark background */
|
|
23
|
+
border: 1px solid rgba(255, 255, 255, 30);
|
|
24
|
+
border-radius: 16px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Title Bar */
|
|
28
|
+
QWidget#titleBar {
|
|
29
|
+
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(70, 70, 70, 255), stop:1 rgba(50, 50, 50, 255));
|
|
30
|
+
border-top-left-radius: 15px;
|
|
31
|
+
border-top-right-radius: 15px;
|
|
32
|
+
border-bottom: 1px solid rgba(0, 0, 0, 50);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
QLabel#titleLabel {
|
|
36
|
+
color: #FFFFFF;
|
|
37
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
38
|
+
font-size: 13px;
|
|
39
|
+
font-weight: bold;
|
|
40
|
+
background-color: transparent;
|
|
41
|
+
padding-left: 5px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
QPushButton#collapseButton {
|
|
45
|
+
background-color: rgba(255, 255, 255, 0);
|
|
46
|
+
color: rgba(255, 255, 255, 180);
|
|
47
|
+
border: none;
|
|
48
|
+
border-radius: 12px;
|
|
49
|
+
font-size: 12px;
|
|
50
|
+
font-weight: bold;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
QPushButton#collapseButton:hover {
|
|
54
|
+
background-color: rgba(255, 255, 255, 30);
|
|
55
|
+
color: #FFFFFF;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Scroll Area */
|
|
59
|
+
QScrollArea#scrollArea {
|
|
60
|
+
background-color: transparent;
|
|
61
|
+
border: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
QWidget#sessionContainer {
|
|
65
|
+
background-color: transparent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Scroll Bar */
|
|
69
|
+
QScrollBar:vertical {
|
|
70
|
+
border: none;
|
|
71
|
+
background: transparent;
|
|
72
|
+
width: 8px;
|
|
73
|
+
margin: 2px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
QScrollBar::handle:vertical {
|
|
77
|
+
background: rgba(255, 255, 255, 40);
|
|
78
|
+
min-height: 20px;
|
|
79
|
+
border-radius: 4px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
QScrollBar::handle:vertical:hover {
|
|
83
|
+
background: rgba(255, 255, 255, 60);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
|
87
|
+
border: none;
|
|
88
|
+
background: none;
|
|
89
|
+
height: 0px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|
93
|
+
background: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Session Card */
|
|
97
|
+
QWidget#sessionCard {
|
|
98
|
+
background-color: rgba(60, 60, 60, 200);
|
|
99
|
+
border: 1px solid rgba(255, 255, 255, 10);
|
|
100
|
+
border-radius: 10px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
QWidget#sessionCard:hover {
|
|
104
|
+
background-color: rgba(75, 75, 75, 230);
|
|
105
|
+
border: 1px solid rgba(255, 255, 255, 25);
|
|
106
|
+
/* Note: QSS does not support box-shadow or transform on hover in the same way CSS does */
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Labels */
|
|
110
|
+
QLabel#projectLabel {
|
|
111
|
+
color: #FF9800; /* Orange */
|
|
112
|
+
font-size: 11px;
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
background-color: transparent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
QLabel#workspaceLabel {
|
|
118
|
+
color: #4CAF50; /* Green */
|
|
119
|
+
font-size: 11px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
background-color: transparent;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
QLabel#stageLabel {
|
|
125
|
+
color: #64B5F6; /* Blue */
|
|
126
|
+
font-size: 11px;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
background-color: transparent;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
QLabel#conversationLabel {
|
|
132
|
+
color: #EEEEEE; /* White-ish */
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
background-color: transparent;
|
|
135
|
+
margin-top: 2px;
|
|
136
|
+
margin-bottom: 4px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Progress Bar */
|
|
140
|
+
QProgressBar#progressBar {
|
|
141
|
+
background-color: rgba(0, 0, 0, 60);
|
|
142
|
+
border: none;
|
|
143
|
+
border-radius: 2px;
|
|
144
|
+
text-align: center;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
QProgressBar#progressBar::chunk {
|
|
148
|
+
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4CAF50, stop:1 #81C784);
|
|
149
|
+
border-radius: 2px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Timer */
|
|
153
|
+
QLabel#timeLabel {
|
|
154
|
+
color: #AAAAAA;
|
|
155
|
+
font-size: 10px;
|
|
156
|
+
font-family: monospace;
|
|
157
|
+
background-color: transparent;
|
|
158
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
窗口位置管理器
|
|
3
|
+
用于管理多个 Feedback UI 窗口的位置,避免重叠
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from typing import Tuple, List, Dict, Optional
|
|
9
|
+
from PySide6.QtGui import QGuiApplication
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WindowPositionManager:
|
|
13
|
+
"""管理多个窗口的位置,实现智能错位显示"""
|
|
14
|
+
|
|
15
|
+
# 窗口位置记录文件
|
|
16
|
+
POSITION_FILE = os.path.join(os.path.dirname(__file__), '.window_positions.json')
|
|
17
|
+
|
|
18
|
+
# 窗口错位的偏移量
|
|
19
|
+
OFFSET_X = 30 # 水平偏移
|
|
20
|
+
OFFSET_Y = 30 # 垂直偏移
|
|
21
|
+
MAX_CASCADE_COUNT = 10 # 最大级联数量
|
|
22
|
+
|
|
23
|
+
# 窗口存活时间(秒),超过这个时间的记录会被清理
|
|
24
|
+
WINDOW_TTL = 300 # 5分钟
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def get_next_position(cls, window_type: str = 'main', default_x: int = None, default_y: int = None) -> Tuple[int, int]:
|
|
28
|
+
"""
|
|
29
|
+
获取下一个窗口的位置
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
window_type: 窗口类型 ('main' 或 'compact')
|
|
33
|
+
default_x: 默认 X 坐标
|
|
34
|
+
default_y: 默认 Y 坐标
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
(x, y) 坐标元组
|
|
38
|
+
"""
|
|
39
|
+
# 清理过期的窗口记录
|
|
40
|
+
cls._cleanup_old_positions()
|
|
41
|
+
|
|
42
|
+
# 读取现有位置记录
|
|
43
|
+
positions = cls._load_positions()
|
|
44
|
+
|
|
45
|
+
# 获取当前窗口类型的记录
|
|
46
|
+
type_positions = positions.get(window_type, [])
|
|
47
|
+
|
|
48
|
+
# 获取屏幕信息
|
|
49
|
+
screen = QGuiApplication.primaryScreen()
|
|
50
|
+
if screen:
|
|
51
|
+
screen_geometry = screen.availableGeometry()
|
|
52
|
+
screen_width = screen_geometry.width()
|
|
53
|
+
screen_height = screen_geometry.height()
|
|
54
|
+
screen_x = screen_geometry.x()
|
|
55
|
+
screen_y = screen_geometry.y()
|
|
56
|
+
else:
|
|
57
|
+
# 默认屏幕尺寸
|
|
58
|
+
screen_width = 1920
|
|
59
|
+
screen_height = 1080
|
|
60
|
+
screen_x = 0
|
|
61
|
+
screen_y = 0
|
|
62
|
+
|
|
63
|
+
# 计算基础位置
|
|
64
|
+
if default_x is None or default_y is None:
|
|
65
|
+
if window_type == 'compact':
|
|
66
|
+
# 精简版默认在顶部居中
|
|
67
|
+
base_x = screen_x + (screen_width - 250) // 2
|
|
68
|
+
base_y = screen_y + 20
|
|
69
|
+
else:
|
|
70
|
+
# 主窗口默认在屏幕中央
|
|
71
|
+
base_x = screen_x + (screen_width - 500) // 2
|
|
72
|
+
base_y = screen_y + (screen_height - 900) // 2
|
|
73
|
+
else:
|
|
74
|
+
base_x = default_x
|
|
75
|
+
base_y = default_y
|
|
76
|
+
|
|
77
|
+
# 计算级联位置
|
|
78
|
+
cascade_index = len(type_positions) % cls.MAX_CASCADE_COUNT
|
|
79
|
+
|
|
80
|
+
# 计算最终位置
|
|
81
|
+
final_x = base_x + (cascade_index * cls.OFFSET_X)
|
|
82
|
+
final_y = base_y + (cascade_index * cls.OFFSET_Y)
|
|
83
|
+
|
|
84
|
+
# 确保窗口不会超出屏幕边界
|
|
85
|
+
window_width = 250 if window_type == 'compact' else 500
|
|
86
|
+
window_height = 40 if window_type == 'compact' else 900
|
|
87
|
+
|
|
88
|
+
# 调整 X 坐标
|
|
89
|
+
if final_x + window_width > screen_x + screen_width:
|
|
90
|
+
final_x = screen_x + screen_width - window_width - 20
|
|
91
|
+
if final_x < screen_x:
|
|
92
|
+
final_x = screen_x + 20
|
|
93
|
+
|
|
94
|
+
# 调整 Y 坐标
|
|
95
|
+
if final_y + window_height > screen_y + screen_height:
|
|
96
|
+
final_y = screen_y + screen_height - window_height - 20
|
|
97
|
+
if final_y < screen_y:
|
|
98
|
+
final_y = screen_y + 20
|
|
99
|
+
|
|
100
|
+
# 记录新位置
|
|
101
|
+
cls._add_position(window_type, final_x, final_y)
|
|
102
|
+
|
|
103
|
+
return (final_x, final_y)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def remove_position(cls, window_type: str, x: int, y: int):
|
|
107
|
+
"""
|
|
108
|
+
移除窗口位置记录(窗口关闭时调用)
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
window_type: 窗口类型
|
|
112
|
+
x: X 坐标
|
|
113
|
+
y: Y 坐标
|
|
114
|
+
"""
|
|
115
|
+
positions = cls._load_positions()
|
|
116
|
+
|
|
117
|
+
if window_type in positions:
|
|
118
|
+
# 移除匹配的位置记录
|
|
119
|
+
positions[window_type] = [
|
|
120
|
+
pos for pos in positions[window_type]
|
|
121
|
+
if not (abs(pos['x'] - x) < 5 and abs(pos['y'] - y) < 5)
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# 如果该类型没有窗口了,删除键
|
|
125
|
+
if not positions[window_type]:
|
|
126
|
+
del positions[window_type]
|
|
127
|
+
|
|
128
|
+
cls._save_positions(positions)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def _load_positions(cls) -> Dict[str, List[Dict]]:
|
|
132
|
+
"""加载位置记录"""
|
|
133
|
+
if os.path.exists(cls.POSITION_FILE):
|
|
134
|
+
try:
|
|
135
|
+
with open(cls.POSITION_FILE, 'r') as f:
|
|
136
|
+
return json.load(f)
|
|
137
|
+
except (json.JSONDecodeError, IOError):
|
|
138
|
+
pass
|
|
139
|
+
return {}
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def _save_positions(cls, positions: Dict[str, List[Dict]]):
|
|
143
|
+
"""保存位置记录"""
|
|
144
|
+
try:
|
|
145
|
+
with open(cls.POSITION_FILE, 'w') as f:
|
|
146
|
+
json.dump(positions, f)
|
|
147
|
+
except IOError:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def _add_position(cls, window_type: str, x: int, y: int):
|
|
152
|
+
"""添加位置记录"""
|
|
153
|
+
positions = cls._load_positions()
|
|
154
|
+
|
|
155
|
+
if window_type not in positions:
|
|
156
|
+
positions[window_type] = []
|
|
157
|
+
|
|
158
|
+
positions[window_type].append({
|
|
159
|
+
'x': x,
|
|
160
|
+
'y': y,
|
|
161
|
+
'timestamp': time.time()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
cls._save_positions(positions)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def _cleanup_old_positions(cls):
|
|
168
|
+
"""清理过期的位置记录"""
|
|
169
|
+
positions = cls._load_positions()
|
|
170
|
+
current_time = time.time()
|
|
171
|
+
updated = False
|
|
172
|
+
|
|
173
|
+
for window_type in list(positions.keys()):
|
|
174
|
+
# 过滤掉过期的记录
|
|
175
|
+
active_positions = [
|
|
176
|
+
pos for pos in positions[window_type]
|
|
177
|
+
if current_time - pos.get('timestamp', 0) < cls.WINDOW_TTL
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
if len(active_positions) != len(positions[window_type]):
|
|
181
|
+
updated = True
|
|
182
|
+
if active_positions:
|
|
183
|
+
positions[window_type] = active_positions
|
|
184
|
+
else:
|
|
185
|
+
del positions[window_type]
|
|
186
|
+
|
|
187
|
+
if updated:
|
|
188
|
+
cls._save_positions(positions)
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def clear_all_positions(cls):
|
|
192
|
+
"""清除所有位置记录(用于调试)"""
|
|
193
|
+
if os.path.exists(cls.POSITION_FILE):
|
|
194
|
+
try:
|
|
195
|
+
os.remove(cls.POSITION_FILE)
|
|
196
|
+
except IOError:
|
|
197
|
+
pass
|
workspace_manager.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
工作空间管理器 - 处理工作空间和阶段信息
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import yaml
|
|
7
|
+
from typing import Optional, Dict, Any, Tuple
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkspaceManager:
|
|
12
|
+
"""工作空间管理器"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, project_path: str = None):
|
|
15
|
+
"""初始化工作空间管理器
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
project_path: 项目路径,如果不提供则使用当前工作目录
|
|
19
|
+
"""
|
|
20
|
+
if project_path:
|
|
21
|
+
self.project_path = Path(project_path)
|
|
22
|
+
else:
|
|
23
|
+
# 尝试找到项目根目录
|
|
24
|
+
current_path = Path.cwd()
|
|
25
|
+
while current_path != current_path.parent:
|
|
26
|
+
if (current_path / '.workspace').exists():
|
|
27
|
+
self.project_path = current_path
|
|
28
|
+
break
|
|
29
|
+
current_path = current_path.parent
|
|
30
|
+
else:
|
|
31
|
+
self.project_path = Path.cwd()
|
|
32
|
+
|
|
33
|
+
self.workspace_dir = self.project_path / '.workspace'
|
|
34
|
+
self.workflows_dir = self.project_path / '.claude' / 'workflows'
|
|
35
|
+
|
|
36
|
+
def get_workspace_id_by_session(self, session_id: str) -> Optional[str]:
|
|
37
|
+
"""根据session_id获取workspace_id
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
session_id: 会话ID
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
workspace_id 或 None
|
|
44
|
+
"""
|
|
45
|
+
session_map_path = self.workspace_dir / 'session_map.json'
|
|
46
|
+
if not session_map_path.exists():
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with open(session_map_path, 'r', encoding='utf-8') as f:
|
|
51
|
+
session_map = json.load(f)
|
|
52
|
+
|
|
53
|
+
# 查找workspace_mappings
|
|
54
|
+
workspace_mappings = session_map.get('workspace_mappings', {})
|
|
55
|
+
return workspace_mappings.get(session_id)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Error reading session_map.json: {e}")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def load_workspace_config(self, workspace_id: str) -> Optional[Dict[str, Any]]:
|
|
61
|
+
"""加载工作空间配置
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
workspace_id: 工作空间ID
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
工作空间配置字典 或 None
|
|
68
|
+
"""
|
|
69
|
+
workspace_path = self.workspace_dir / workspace_id / 'workspace.yml'
|
|
70
|
+
if not workspace_path.exists():
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
with open(workspace_path, 'r', encoding='utf-8') as f:
|
|
75
|
+
return yaml.safe_load(f)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f"Error reading workspace.yml: {e}")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def load_stage_template(self, template_id: str) -> Optional[Dict[str, Any]]:
|
|
81
|
+
"""加载阶段模板
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
template_id: 模板ID或完整文件路径
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
阶段模板配置 或 None
|
|
88
|
+
"""
|
|
89
|
+
# 判断是否为完整路径
|
|
90
|
+
if '/' in template_id or template_id.endswith('.yml'):
|
|
91
|
+
# 直接使用路径
|
|
92
|
+
template_path = Path(template_id)
|
|
93
|
+
else:
|
|
94
|
+
# 兼容旧逻辑:拼接路径
|
|
95
|
+
template_path = self.workflows_dir / f'{template_id}.yml'
|
|
96
|
+
|
|
97
|
+
if not template_path.exists():
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
with open(template_path, 'r', encoding='utf-8') as f:
|
|
102
|
+
return yaml.safe_load(f)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Error reading stage template: {e}")
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def get_stage_info(self, session_id: str = None, workspace_id: str = None) -> Optional[Dict[str, Any]]:
|
|
108
|
+
"""获取阶段信息
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
session_id: 会话ID(如果提供了workspace_id则可选)
|
|
112
|
+
workspace_id: 工作空间ID(优先使用,如果不提供则通过session_id查找)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
包含当前阶段、上一阶段、下一阶段信息的字典
|
|
116
|
+
"""
|
|
117
|
+
# 1. 获取workspace_id
|
|
118
|
+
if not workspace_id:
|
|
119
|
+
if not session_id:
|
|
120
|
+
return None
|
|
121
|
+
workspace_id = self.get_workspace_id_by_session(session_id)
|
|
122
|
+
if not workspace_id:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# 2. 加载workspace配置
|
|
126
|
+
workspace_config = self.load_workspace_config(workspace_id)
|
|
127
|
+
if not workspace_config:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
# 3. 获取阶段模板和当前阶段
|
|
131
|
+
stage_template_id = workspace_config.get('stage_template_id')
|
|
132
|
+
current_stage_id = workspace_config.get('current_stage_id')
|
|
133
|
+
|
|
134
|
+
if not stage_template_id or not current_stage_id:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# 4. 加载阶段模板
|
|
138
|
+
template_config = self.load_stage_template(stage_template_id)
|
|
139
|
+
if not template_config:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# 5. 解析阶段信息
|
|
143
|
+
workflow = template_config.get('workflow', {})
|
|
144
|
+
steps = workflow.get('steps', [])
|
|
145
|
+
|
|
146
|
+
if not steps:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# 找到当前阶段索引
|
|
150
|
+
current_index = -1
|
|
151
|
+
for i, step in enumerate(steps):
|
|
152
|
+
if step.get('id') == current_stage_id:
|
|
153
|
+
current_index = i
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
if current_index == -1:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# 构建阶段信息
|
|
160
|
+
result = {
|
|
161
|
+
'workspace_id': workspace_id,
|
|
162
|
+
'current_stage': {
|
|
163
|
+
'id': steps[current_index].get('id'),
|
|
164
|
+
'title': steps[current_index].get('title'),
|
|
165
|
+
'description': steps[current_index].get('des')
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# 上一阶段
|
|
170
|
+
if current_index > 0:
|
|
171
|
+
result['prev_stage'] = {
|
|
172
|
+
'id': steps[current_index - 1].get('id'),
|
|
173
|
+
'title': steps[current_index - 1].get('title'),
|
|
174
|
+
'description': steps[current_index - 1].get('des')
|
|
175
|
+
}
|
|
176
|
+
else:
|
|
177
|
+
result['prev_stage'] = None
|
|
178
|
+
|
|
179
|
+
# 下一阶段
|
|
180
|
+
if current_index < len(steps) - 1:
|
|
181
|
+
result['next_stage'] = {
|
|
182
|
+
'id': steps[current_index + 1].get('id'),
|
|
183
|
+
'title': steps[current_index + 1].get('title'),
|
|
184
|
+
'description': steps[current_index + 1].get('des')
|
|
185
|
+
}
|
|
186
|
+
else:
|
|
187
|
+
result['next_stage'] = None
|
|
188
|
+
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_stage_info_for_session(session_id: str, project_path: str = None) -> Optional[Dict[str, Any]]:
|
|
193
|
+
"""便捷函数:获取指定会话的阶段信息
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
session_id: 会话ID
|
|
197
|
+
project_path: 项目路径
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
阶段信息字典
|
|
201
|
+
"""
|
|
202
|
+
manager = WorkspaceManager(project_path)
|
|
203
|
+
return manager.get_stage_info(session_id)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_workspace_goal_for_session(session_id: str, project_path: str = None) -> Optional[str]:
|
|
207
|
+
"""便捷函数:获取指定会话的工作空间goal
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
session_id: 会话ID
|
|
211
|
+
project_path: 项目路径
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
工作空间goal或None
|
|
215
|
+
"""
|
|
216
|
+
manager = WorkspaceManager(project_path)
|
|
217
|
+
workspace_id = manager.get_workspace_id_by_session(session_id)
|
|
218
|
+
if not workspace_id:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
workspace_config = manager.load_workspace_config(workspace_id)
|
|
222
|
+
if not workspace_config:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
return workspace_config.get('goal')
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_session_title_for_session(session_id: str, project_path: str = None) -> Optional[str]:
|
|
229
|
+
"""便捷函数:获取指定会话的对话标题
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
session_id: 会话ID
|
|
233
|
+
project_path: 项目路径
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
对话标题或None
|
|
237
|
+
"""
|
|
238
|
+
manager = WorkspaceManager(project_path)
|
|
239
|
+
workspace_id = manager.get_workspace_id_by_session(session_id)
|
|
240
|
+
if not workspace_id:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
workspace_config = manager.load_workspace_config(workspace_id)
|
|
244
|
+
if not workspace_config:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
# 从sessions列表中查找匹配的session
|
|
248
|
+
sessions = workspace_config.get('sessions', [])
|
|
249
|
+
for session in sessions:
|
|
250
|
+
if session.get('id') == session_id:
|
|
251
|
+
return session.get('title')
|
|
252
|
+
|
|
253
|
+
return None
|