cloneloop 0.1.0__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.
- cloneloop-0.1.0.dist-info/METADATA +392 -0
- cloneloop-0.1.0.dist-info/RECORD +118 -0
- cloneloop-0.1.0.dist-info/WHEEL +4 -0
- cloneloop-0.1.0.dist-info/entry_points.txt +5 -0
- mcp_ai_supervisor/__init__.py +52 -0
- mcp_ai_supervisor/__main__.py +551 -0
- mcp_ai_supervisor/debug.py +15 -0
- mcp_ai_supervisor/desktop_app/__init__.py +29 -0
- mcp_ai_supervisor/desktop_app/desktop_app.py +390 -0
- mcp_ai_supervisor/helpers/__init__.py +4 -0
- mcp_ai_supervisor/helpers/feedback_helpers.py +140 -0
- mcp_ai_supervisor/helpers/image_helpers.py +73 -0
- mcp_ai_supervisor/i18n.py +14 -0
- mcp_ai_supervisor/log_writer.py +16 -0
- mcp_ai_supervisor/logging/__init__.py +8 -0
- mcp_ai_supervisor/logging/log_parser.py +293 -0
- mcp_ai_supervisor/logging/log_tailer.py +367 -0
- mcp_ai_supervisor/py.typed +0 -0
- mcp_ai_supervisor/server.py +654 -0
- mcp_ai_supervisor/utils/__init__.py +28 -0
- mcp_ai_supervisor/utils/error_handler.py +10 -0
- mcp_ai_supervisor/utils/memory_monitor.py +11 -0
- mcp_ai_supervisor/utils/paths.py +7 -0
- mcp_ai_supervisor/utils/resource_manager.py +15 -0
- mcp_ai_supervisor/utils/structure_checker.py +291 -0
- mcp_ai_supervisor/web/__init__.py +26 -0
- mcp_ai_supervisor/web/constants/__init__.py +8 -0
- mcp_ai_supervisor/web/constants/message_codes.py +173 -0
- mcp_ai_supervisor/web/core/__init__.py +30 -0
- mcp_ai_supervisor/web/core/agent_bridge.py +957 -0
- mcp_ai_supervisor/web/core/auto_responder.py +332 -0
- mcp_ai_supervisor/web/core/context_collector.py +124 -0
- mcp_ai_supervisor/web/core/conversation_recorder.py +751 -0
- mcp_ai_supervisor/web/core/delegate_manager.py +1193 -0
- mcp_ai_supervisor/web/core/feedback_mediator.py +259 -0
- mcp_ai_supervisor/web/core/message_channel.py +551 -0
- mcp_ai_supervisor/web/core/response_builder.py +333 -0
- mcp_ai_supervisor/web/core/scene_detector.py +184 -0
- mcp_ai_supervisor/web/core/task_state.py +194 -0
- mcp_ai_supervisor/web/locales/en/translation.json +643 -0
- mcp_ai_supervisor/web/locales/zh-CN/translation.json +629 -0
- mcp_ai_supervisor/web/locales/zh-TW/translation.json +648 -0
- mcp_ai_supervisor/web/main.py +747 -0
- mcp_ai_supervisor/web/managers/__init__.py +8 -0
- mcp_ai_supervisor/web/managers/desktop_manager.py +228 -0
- mcp_ai_supervisor/web/managers/server_manager.py +170 -0
- mcp_ai_supervisor/web/managers/session_manager.py +515 -0
- mcp_ai_supervisor/web/managers/workbench_connector.py +186 -0
- mcp_ai_supervisor/web/models/__init__.py +13 -0
- mcp_ai_supervisor/web/models/feedback_result.py +16 -0
- mcp_ai_supervisor/web/models/feedback_session.py +938 -0
- mcp_ai_supervisor/web/models/session_cleaner.py +245 -0
- mcp_ai_supervisor/web/models/session_commands.py +213 -0
- mcp_ai_supervisor/web/models/session_resolver.py +158 -0
- mcp_ai_supervisor/web/models/session_timer.py +242 -0
- mcp_ai_supervisor/web/routes/__init__.py +12 -0
- mcp_ai_supervisor/web/routes/main_routes.py +965 -0
- mcp_ai_supervisor/web/routes/settings_routes.py +195 -0
- mcp_ai_supervisor/web/routes/ws_handlers.py +270 -0
- mcp_ai_supervisor/web/static/css/audio-management.css +545 -0
- mcp_ai_supervisor/web/static/css/notification-settings.css +152 -0
- mcp_ai_supervisor/web/static/css/prompt-management.css +566 -0
- mcp_ai_supervisor/web/static/css/session-management.css +1428 -0
- mcp_ai_supervisor/web/static/css/styles.css +2267 -0
- mcp_ai_supervisor/web/static/favicon.ico +0 -0
- mcp_ai_supervisor/web/static/icon-192.png +0 -0
- mcp_ai_supervisor/web/static/icon.svg +11 -0
- mcp_ai_supervisor/web/static/index.html +37 -0
- mcp_ai_supervisor/web/static/js/app.js +1721 -0
- mcp_ai_supervisor/web/static/js/i18n.js +376 -0
- mcp_ai_supervisor/web/static/js/modules/audio/audio-manager.js +610 -0
- mcp_ai_supervisor/web/static/js/modules/audio/audio-settings-ui.js +732 -0
- mcp_ai_supervisor/web/static/js/modules/connection-monitor.js +435 -0
- mcp_ai_supervisor/web/static/js/modules/constants/message-codes.js +168 -0
- mcp_ai_supervisor/web/static/js/modules/countdown-manager.js +273 -0
- mcp_ai_supervisor/web/static/js/modules/drag-drop-handler.js +677 -0
- mcp_ai_supervisor/web/static/js/modules/file-upload-manager.js +555 -0
- mcp_ai_supervisor/web/static/js/modules/image-handler.js +199 -0
- mcp_ai_supervisor/web/static/js/modules/logger.js +404 -0
- mcp_ai_supervisor/web/static/js/modules/notification/notification-manager.js +360 -0
- mcp_ai_supervisor/web/static/js/modules/notification/notification-settings.js +344 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-input-buttons.js +427 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-manager.js +414 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-modal.js +458 -0
- mcp_ai_supervisor/web/static/js/modules/prompt/prompt-settings-ui.js +524 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-data-manager.js +1042 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-details-modal.js +594 -0
- mcp_ai_supervisor/web/static/js/modules/session/session-ui-renderer.js +836 -0
- mcp_ai_supervisor/web/static/js/modules/session-manager.js +1059 -0
- mcp_ai_supervisor/web/static/js/modules/settings-manager.js +1002 -0
- mcp_ai_supervisor/web/static/js/modules/tab-manager.js +235 -0
- mcp_ai_supervisor/web/static/js/modules/textarea-height-manager.js +267 -0
- mcp_ai_supervisor/web/static/js/modules/ui-manager.js +578 -0
- mcp_ai_supervisor/web/static/js/modules/utils/dom-utils.js +392 -0
- mcp_ai_supervisor/web/static/js/modules/utils/status-utils.js +403 -0
- mcp_ai_supervisor/web/static/js/modules/utils/time-utils.js +440 -0
- mcp_ai_supervisor/web/static/js/modules/utils.js +557 -0
- mcp_ai_supervisor/web/static/js/modules/websocket-manager.js +875 -0
- mcp_ai_supervisor/web/static/js/modules/workbench-notification.js +103 -0
- mcp_ai_supervisor/web/static/js/vendor/marked.min.js +6 -0
- mcp_ai_supervisor/web/static/js/vendor/purify.min.js +3 -0
- mcp_ai_supervisor/web/templates/components/image-upload.html +43 -0
- mcp_ai_supervisor/web/templates/components/settings-card.html +58 -0
- mcp_ai_supervisor/web/templates/components/status-indicator.html +31 -0
- mcp_ai_supervisor/web/templates/components/toggle-switch.html +19 -0
- mcp_ai_supervisor/web/templates/feedback.html +1198 -0
- mcp_ai_supervisor/web/templates/index.html +378 -0
- mcp_ai_supervisor/web/utils/__init__.py +12 -0
- mcp_ai_supervisor/web/utils/browser.py +35 -0
- mcp_ai_supervisor/web/utils/compression_config.py +195 -0
- mcp_ai_supervisor/web/utils/compression_monitor.py +314 -0
- mcp_ai_supervisor/web/utils/ide_opener.py +286 -0
- mcp_ai_supervisor/web/utils/network.py +66 -0
- mcp_ai_supervisor/web/utils/port_manager.py +340 -0
- mcp_ai_supervisor/web/utils/session_cleanup_manager.py +543 -0
- mcp_ai_supervisor/web/utils/workbench_client.py +899 -0
- mcp_ai_supervisor/workbench/__init__.py +5 -0
- mcp_ai_supervisor/workbench/__main__.py +5 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
自动回复生成器模块(降级方案 D7)
|
|
4
|
+
================================
|
|
5
|
+
|
|
6
|
+
当 Agent(qodercli)不可用时的降级回复生成器。
|
|
7
|
+
|
|
8
|
+
- MCP_AGENT_ENABLED=false → 直接走 AutoResponder
|
|
9
|
+
- Agent 调用失败/超时 → 降级走 AutoResponder
|
|
10
|
+
|
|
11
|
+
主要功能:
|
|
12
|
+
- 检测问句类型(确认、建议、选择、澄清等)
|
|
13
|
+
- 生成代码自检提示(当检测到代码变更时)
|
|
14
|
+
- 加载项目规范(.cursor/rules/ 或 .qoder/rules/)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from .scene_detector import SceneResult
|
|
23
|
+
from ...debug import server_debug_log as debug_log
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class QuestionType(Enum):
|
|
27
|
+
"""问句类型枚举"""
|
|
28
|
+
|
|
29
|
+
NONE = "none" # 非问句
|
|
30
|
+
CONFIRMATION = "confirmation" # 确认类(是否、要不要)
|
|
31
|
+
SUGGESTION = "suggestion" # 建议类(我建议、推荐)
|
|
32
|
+
CHOICE = "choice" # 选择类(A还是B)
|
|
33
|
+
CLARIFICATION = "clarification" # 澄清类(什么意思、为什么)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AutoResponder:
|
|
37
|
+
"""
|
|
38
|
+
自动回复生成器
|
|
39
|
+
|
|
40
|
+
负责在 auto 模式下自动生成回复内容,包括:
|
|
41
|
+
- 检测问句类型并生成对应回复
|
|
42
|
+
- 检测代码变更并生成自检提示
|
|
43
|
+
- 加载项目规范并追加到响应中
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def generate(
|
|
47
|
+
self,
|
|
48
|
+
scene: SceneResult,
|
|
49
|
+
summary: str,
|
|
50
|
+
intention: str,
|
|
51
|
+
project_dir: str = "",
|
|
52
|
+
) -> str:
|
|
53
|
+
"""
|
|
54
|
+
生成自动回复内容
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
scene: 场景检测结果
|
|
58
|
+
summary: 工作摘要
|
|
59
|
+
intention: AI 意图/回复内容
|
|
60
|
+
project_dir: 项目目录路径(可选)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: 自动生成的回复内容
|
|
64
|
+
|
|
65
|
+
逻辑:
|
|
66
|
+
- 如果 scene.has_code_changes 为 True:返回代码自检提示
|
|
67
|
+
- 否则:检测问句类型,返回对应回复
|
|
68
|
+
"""
|
|
69
|
+
if scene.has_code_changes:
|
|
70
|
+
return self._build_code_review_prompt(scene, project_dir)
|
|
71
|
+
|
|
72
|
+
question_type = self._detect_question(summary, intention)
|
|
73
|
+
return self._get_response_for_question_type(question_type)
|
|
74
|
+
|
|
75
|
+
def _detect_question(self, summary: str, intention: str) -> QuestionType:
|
|
76
|
+
"""
|
|
77
|
+
检测问句类型
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
summary: 工作摘要
|
|
81
|
+
intention: AI 意图/回复内容
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
QuestionType: 检测到的问句类型
|
|
85
|
+
|
|
86
|
+
检测逻辑:
|
|
87
|
+
1. 获取 intention 或 summary 的最后 300 字符
|
|
88
|
+
2. 检测关键词匹配
|
|
89
|
+
3. 检测问号标记
|
|
90
|
+
4. 返回匹配的类型
|
|
91
|
+
"""
|
|
92
|
+
# 获取最后 300 字符用于分析
|
|
93
|
+
text = intention if intention else summary
|
|
94
|
+
if not text:
|
|
95
|
+
return QuestionType.NONE
|
|
96
|
+
|
|
97
|
+
# 取最后 300 字符
|
|
98
|
+
analysis_text = text[-300:] if len(text) > 300 else text
|
|
99
|
+
analysis_text_lower = analysis_text.lower()
|
|
100
|
+
|
|
101
|
+
# 确认类关键词
|
|
102
|
+
confirmation_keywords = [
|
|
103
|
+
"是否",
|
|
104
|
+
"要不要",
|
|
105
|
+
"确认",
|
|
106
|
+
"可以吗",
|
|
107
|
+
"行吗",
|
|
108
|
+
"好吗",
|
|
109
|
+
"对吗",
|
|
110
|
+
"shall",
|
|
111
|
+
"should we",
|
|
112
|
+
"confirm",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
# 建议类关键词
|
|
116
|
+
suggestion_keywords = [
|
|
117
|
+
"建议",
|
|
118
|
+
"推荐",
|
|
119
|
+
"suggest",
|
|
120
|
+
"recommend",
|
|
121
|
+
"方案",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# 选择类关键词
|
|
125
|
+
choice_keywords = [
|
|
126
|
+
"还是",
|
|
127
|
+
"或者",
|
|
128
|
+
"a方案",
|
|
129
|
+
"b方案",
|
|
130
|
+
"方案一",
|
|
131
|
+
"方案二",
|
|
132
|
+
"which",
|
|
133
|
+
"choose",
|
|
134
|
+
"option",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# 澄清类关键词
|
|
138
|
+
clarification_keywords = [
|
|
139
|
+
"什么意思",
|
|
140
|
+
"为什么",
|
|
141
|
+
"怎么",
|
|
142
|
+
"如何",
|
|
143
|
+
"能解释",
|
|
144
|
+
"what",
|
|
145
|
+
"why",
|
|
146
|
+
"how",
|
|
147
|
+
"explain",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
# 检测关键词
|
|
151
|
+
for keyword in confirmation_keywords:
|
|
152
|
+
if keyword in analysis_text_lower:
|
|
153
|
+
return QuestionType.CONFIRMATION
|
|
154
|
+
|
|
155
|
+
for keyword in suggestion_keywords:
|
|
156
|
+
if keyword in analysis_text_lower:
|
|
157
|
+
return QuestionType.SUGGESTION
|
|
158
|
+
|
|
159
|
+
for keyword in choice_keywords:
|
|
160
|
+
if keyword in analysis_text_lower:
|
|
161
|
+
return QuestionType.CHOICE
|
|
162
|
+
|
|
163
|
+
for keyword in clarification_keywords:
|
|
164
|
+
if keyword in analysis_text_lower:
|
|
165
|
+
return QuestionType.CLARIFICATION
|
|
166
|
+
|
|
167
|
+
# 检测问号
|
|
168
|
+
has_question_mark = "?" in analysis_text or "?" in analysis_text
|
|
169
|
+
|
|
170
|
+
# 如果只有问号没有关键词,返回澄清类
|
|
171
|
+
if has_question_mark:
|
|
172
|
+
return QuestionType.CLARIFICATION
|
|
173
|
+
|
|
174
|
+
# 否则返回非问句
|
|
175
|
+
return QuestionType.NONE
|
|
176
|
+
|
|
177
|
+
def _get_response_for_question_type(self, question_type: QuestionType) -> str:
|
|
178
|
+
"""
|
|
179
|
+
根据问句类型返回对应回复
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
question_type: 问句类型
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
str: 对应的回复内容
|
|
186
|
+
"""
|
|
187
|
+
responses = {
|
|
188
|
+
QuestionType.CONFIRMATION: "是的,请继续执行。",
|
|
189
|
+
QuestionType.SUGGESTION: "好的,按照你建议的方案继续执行。",
|
|
190
|
+
QuestionType.CHOICE: "请根据你的专业判断选择最佳方案。",
|
|
191
|
+
QuestionType.CLARIFICATION: "请根据你的专业判断决定。",
|
|
192
|
+
QuestionType.NONE: "✅ 本次检查通过,请继续执行。",
|
|
193
|
+
}
|
|
194
|
+
return responses.get(question_type, "✅ 本次检查通过,请继续执行。")
|
|
195
|
+
|
|
196
|
+
def _build_code_review_prompt(
|
|
197
|
+
self, scene: SceneResult, project_dir: str = ""
|
|
198
|
+
) -> str:
|
|
199
|
+
"""
|
|
200
|
+
构建代码自检提示
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
scene: 场景检测结果
|
|
204
|
+
project_dir: 项目目录路径(可选)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
str: 代码自检提示文本
|
|
208
|
+
|
|
209
|
+
包含内容:
|
|
210
|
+
1. 变更文件列表(最多10个)
|
|
211
|
+
2. 4个自检检查点
|
|
212
|
+
3. 项目规范(如果存在)
|
|
213
|
+
"""
|
|
214
|
+
# 列出变更文件(最多10个)
|
|
215
|
+
changed_files = scene.changed_files[:10]
|
|
216
|
+
file_list_text = "\n".join(f"- {f}" for f in changed_files)
|
|
217
|
+
|
|
218
|
+
if len(scene.changed_files) > 10:
|
|
219
|
+
file_list_text += f"\n...等共 {len(scene.changed_files)} 个文件"
|
|
220
|
+
|
|
221
|
+
# 构建自检提示
|
|
222
|
+
prompt = f"""━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
🔍 【代码自检提示】
|
|
224
|
+
|
|
225
|
+
检测到代码变更,请自检以下内容:
|
|
226
|
+
|
|
227
|
+
📁 变更文件:
|
|
228
|
+
{file_list_text}
|
|
229
|
+
|
|
230
|
+
✅ 请检查:
|
|
231
|
+
1. 功能完整性:是否完成了要求的所有功能?
|
|
232
|
+
2. 代码质量:是否有明显的 bug 或安全问题?
|
|
233
|
+
3. 向后兼容:是否破坏了现有功能?
|
|
234
|
+
4. 测试覆盖:关键逻辑是否有测试?
|
|
235
|
+
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
# 加载项目规范
|
|
239
|
+
project_rules = self._load_project_rules(project_dir)
|
|
240
|
+
if project_rules:
|
|
241
|
+
prompt += f"""📋 项目规范:
|
|
242
|
+
{project_rules}
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
prompt += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
247
|
+
|
|
248
|
+
return prompt
|
|
249
|
+
|
|
250
|
+
def _load_project_rules(self, project_dir: str) -> str:
|
|
251
|
+
"""
|
|
252
|
+
加载项目规范
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
project_dir: 项目目录路径
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
str: 项目规范内容(限制2000字符),如果不存在或出错则返回空字符串
|
|
259
|
+
|
|
260
|
+
加载逻辑:
|
|
261
|
+
1. 优先检查 .cursor/rules/ 目录
|
|
262
|
+
2. 其次检查 .qoder/rules/ 目录
|
|
263
|
+
3. 读取所有 .md、.mdc、.txt 文件
|
|
264
|
+
4. 拼接内容,限制总长度 2000 字符
|
|
265
|
+
"""
|
|
266
|
+
if not project_dir:
|
|
267
|
+
return ""
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
project_path = Path(project_dir)
|
|
271
|
+
if not project_path.exists():
|
|
272
|
+
debug_log(f"项目目录不存在: {project_dir}")
|
|
273
|
+
return ""
|
|
274
|
+
|
|
275
|
+
# 优先检查 .cursor/rules/
|
|
276
|
+
rules_dirs = [
|
|
277
|
+
project_path / ".cursor" / "rules",
|
|
278
|
+
project_path / ".qoder" / "rules",
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
rules_dir = None
|
|
282
|
+
for dir_path in rules_dirs:
|
|
283
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
284
|
+
rules_dir = dir_path
|
|
285
|
+
debug_log(f"找到项目规范目录: {rules_dir}")
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
if not rules_dir:
|
|
289
|
+
debug_log("未找到项目规范目录")
|
|
290
|
+
return ""
|
|
291
|
+
|
|
292
|
+
# 读取所有 .md、.mdc、.txt 文件
|
|
293
|
+
rule_files = []
|
|
294
|
+
for ext in [".md", ".mdc", ".txt"]:
|
|
295
|
+
rule_files.extend(rules_dir.glob(f"*{ext}"))
|
|
296
|
+
|
|
297
|
+
if not rule_files:
|
|
298
|
+
debug_log("项目规范目录中没有找到规则文件")
|
|
299
|
+
return ""
|
|
300
|
+
|
|
301
|
+
# 拼接内容
|
|
302
|
+
content_parts = []
|
|
303
|
+
total_length = 0
|
|
304
|
+
max_length = 2000
|
|
305
|
+
|
|
306
|
+
for rule_file in sorted(rule_files):
|
|
307
|
+
try:
|
|
308
|
+
file_content = rule_file.read_text(encoding="utf-8")
|
|
309
|
+
file_length = len(file_content)
|
|
310
|
+
|
|
311
|
+
# 如果加上这个文件会超过限制,只取部分内容
|
|
312
|
+
if total_length + file_length > max_length:
|
|
313
|
+
remaining = max_length - total_length
|
|
314
|
+
if remaining > 100: # 至少保留100字符才有意义
|
|
315
|
+
content_parts.append(file_content[:remaining])
|
|
316
|
+
content_parts.append("\n...")
|
|
317
|
+
break
|
|
318
|
+
|
|
319
|
+
content_parts.append(f"# {rule_file.name}\n{file_content}\n")
|
|
320
|
+
total_length += file_length + len(rule_file.name) + 3
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
debug_log(f"读取规则文件失败 {rule_file}: {e}")
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
result = "\n".join(content_parts)
|
|
327
|
+
debug_log(f"成功加载项目规范,总长度: {len(result)} 字符")
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
except Exception as e:
|
|
331
|
+
debug_log(f"加载项目规范时出错: {e}")
|
|
332
|
+
return ""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
项目上下文收集器模块
|
|
4
|
+
===================
|
|
5
|
+
|
|
6
|
+
为 Agent 提供完整的监督上下文,包括:
|
|
7
|
+
- 项目规则和技能目录路径(D64:不读内容,传路径)
|
|
8
|
+
- AI 当前轮行为数据(summary/intention/changed_files)
|
|
9
|
+
- 任务状态迭代信息
|
|
10
|
+
|
|
11
|
+
设计决策参考:
|
|
12
|
+
- D25: 仅传当前轮 summary/intention
|
|
13
|
+
- D36: diff 统一传路径委托 Agent
|
|
14
|
+
- D52: changed_files_all 上限 200
|
|
15
|
+
- D64: Prompt 只传路径和元数据
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContextCollector:
|
|
27
|
+
"""项目上下文收集器,为 Agent 提供完整的监督上下文"""
|
|
28
|
+
|
|
29
|
+
MAX_CHANGED_FILES = 200
|
|
30
|
+
|
|
31
|
+
def __init__(self, project_dir: str) -> None:
|
|
32
|
+
self.project_dir = project_dir
|
|
33
|
+
self.project_rules_dir: str = ""
|
|
34
|
+
self.project_skills_dirs: list[str] = []
|
|
35
|
+
self.sub_agent_skill_paths: list[str] = []
|
|
36
|
+
self.current_summary: str = ""
|
|
37
|
+
self.current_intention: str = ""
|
|
38
|
+
self.changed_files_all: list[str] = []
|
|
39
|
+
self._current_changed: list[str] = []
|
|
40
|
+
self._dirs_scanned: bool = False
|
|
41
|
+
|
|
42
|
+
def update(
|
|
43
|
+
self,
|
|
44
|
+
summary: str,
|
|
45
|
+
intention: str,
|
|
46
|
+
changed_files: list[str] | None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""每轮调用时更新当前轮数据(D25: 不累计历史 summary/intention)"""
|
|
49
|
+
logger.debug(
|
|
50
|
+
"[ContextCollector] update: summary=%d intention=%d files=%d",
|
|
51
|
+
len(summary or ""),
|
|
52
|
+
len(intention or ""),
|
|
53
|
+
len(changed_files or []),
|
|
54
|
+
)
|
|
55
|
+
self.current_summary = summary
|
|
56
|
+
self.current_intention = intention
|
|
57
|
+
self._current_changed = list(changed_files) if changed_files else []
|
|
58
|
+
# D52/R23: 有序列表去重,保留最后一次出现的位置
|
|
59
|
+
for f in self._current_changed:
|
|
60
|
+
if f in self.changed_files_all:
|
|
61
|
+
self.changed_files_all.remove(f)
|
|
62
|
+
self.changed_files_all.append(f)
|
|
63
|
+
if len(self.changed_files_all) > self.MAX_CHANGED_FILES:
|
|
64
|
+
self.changed_files_all = self.changed_files_all[-self.MAX_CHANGED_FILES :]
|
|
65
|
+
|
|
66
|
+
async def collect(self, task_state) -> dict:
|
|
67
|
+
"""收集完整上下文,输出结构化 dict(D64: 只输出路径元数据)。"""
|
|
68
|
+
logger.info("[ContextCollector] collect 开始: project=%s", self.project_dir)
|
|
69
|
+
if not self._dirs_scanned:
|
|
70
|
+
self._scan_project_dirs()
|
|
71
|
+
self._dirs_scanned = True
|
|
72
|
+
logger.debug(
|
|
73
|
+
"[ContextCollector] collect 完成: rules=%s, all_files=%d",
|
|
74
|
+
self.project_rules_dir or "N/A",
|
|
75
|
+
len(self.changed_files_all),
|
|
76
|
+
)
|
|
77
|
+
return {
|
|
78
|
+
"project_dir": self.project_dir,
|
|
79
|
+
"original_task": task_state.original_task,
|
|
80
|
+
"retry_count": task_state.retry_count,
|
|
81
|
+
"task_phase": task_state.task_phase.value,
|
|
82
|
+
"completion_attempts": task_state.agent_deny_count,
|
|
83
|
+
"agent_deny_count": task_state.agent_deny_count,
|
|
84
|
+
"current_summary": self.current_summary,
|
|
85
|
+
"current_intention": self.current_intention,
|
|
86
|
+
"changed_files_this_round": list(self._current_changed),
|
|
87
|
+
"changed_files_all": list(self.changed_files_all),
|
|
88
|
+
"project_rules_dir": self.project_rules_dir,
|
|
89
|
+
"project_skills_dirs": self.project_skills_dirs,
|
|
90
|
+
"sub_agent_skill_paths": list(self.sub_agent_skill_paths),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# ─── 项目目录扫描(T1.1)──────────────────────────────
|
|
94
|
+
|
|
95
|
+
def _scan_project_dirs(self) -> None:
|
|
96
|
+
"""扫描项目目录,记录规则和技能目录路径(D64: 只传路径,不读内容)"""
|
|
97
|
+
logger.debug("[ContextCollector] 扫描项目目录: %s", self.project_dir)
|
|
98
|
+
rules_dir = os.path.join(self.project_dir, ".cursor", "rules")
|
|
99
|
+
if os.path.isdir(rules_dir):
|
|
100
|
+
self.project_rules_dir = rules_dir
|
|
101
|
+
|
|
102
|
+
for skills_dir in [
|
|
103
|
+
os.path.join(self.project_dir, ".cursor", "skills"),
|
|
104
|
+
os.path.join(self.project_dir, ".agents", "skills"),
|
|
105
|
+
]:
|
|
106
|
+
if os.path.isdir(skills_dir):
|
|
107
|
+
self.project_skills_dirs.append(skills_dir)
|
|
108
|
+
|
|
109
|
+
self.sub_agent_skill_paths = []
|
|
110
|
+
scan_dirs = list(self.project_skills_dirs)
|
|
111
|
+
global_qoder_skills = os.path.join(os.path.expanduser("~"), ".qoder", "skills")
|
|
112
|
+
if os.path.isdir(global_qoder_skills) and global_qoder_skills not in scan_dirs:
|
|
113
|
+
scan_dirs.append(global_qoder_skills)
|
|
114
|
+
|
|
115
|
+
seen_skill_names: set[str] = set()
|
|
116
|
+
for skills_dir in scan_dirs:
|
|
117
|
+
if not os.path.isdir(skills_dir):
|
|
118
|
+
continue
|
|
119
|
+
for entry in sorted(os.listdir(skills_dir)):
|
|
120
|
+
base_name = entry.removeprefix("tbj_")
|
|
121
|
+
skill_file = os.path.join(skills_dir, entry, "SKILL.md")
|
|
122
|
+
if os.path.isfile(skill_file) and base_name not in seen_skill_names:
|
|
123
|
+
self.sub_agent_skill_paths.append(skill_file)
|
|
124
|
+
seen_skill_names.add(base_name)
|