super-dev 2.0.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.
- super_dev/__init__.py +11 -0
- super_dev/analyzer/__init__.py +34 -0
- super_dev/analyzer/analyzer.py +440 -0
- super_dev/analyzer/detectors.py +511 -0
- super_dev/analyzer/models.py +285 -0
- super_dev/cli.py +3257 -0
- super_dev/config/__init__.py +11 -0
- super_dev/config/frontend.py +557 -0
- super_dev/config/manager.py +281 -0
- super_dev/creators/__init__.py +26 -0
- super_dev/creators/creator.py +134 -0
- super_dev/creators/document_generator.py +2473 -0
- super_dev/creators/frontend_builder.py +371 -0
- super_dev/creators/implementation_builder.py +789 -0
- super_dev/creators/prompt_generator.py +289 -0
- super_dev/creators/requirement_parser.py +354 -0
- super_dev/creators/spec_builder.py +195 -0
- super_dev/deployers/__init__.py +20 -0
- super_dev/deployers/cicd.py +1269 -0
- super_dev/deployers/delivery.py +229 -0
- super_dev/deployers/migration.py +1032 -0
- super_dev/design/__init__.py +74 -0
- super_dev/design/aesthetics.py +530 -0
- super_dev/design/charts.py +396 -0
- super_dev/design/codegen.py +379 -0
- super_dev/design/engine.py +528 -0
- super_dev/design/generator.py +395 -0
- super_dev/design/landing.py +422 -0
- super_dev/design/tech_stack.py +524 -0
- super_dev/design/tokens.py +269 -0
- super_dev/design/ux_guide.py +391 -0
- super_dev/exceptions.py +119 -0
- super_dev/experts/__init__.py +19 -0
- super_dev/experts/service.py +161 -0
- super_dev/integrations/__init__.py +7 -0
- super_dev/integrations/manager.py +264 -0
- super_dev/orchestrator/__init__.py +12 -0
- super_dev/orchestrator/engine.py +958 -0
- super_dev/orchestrator/experts.py +423 -0
- super_dev/orchestrator/knowledge.py +352 -0
- super_dev/orchestrator/quality.py +356 -0
- super_dev/reviewers/__init__.py +17 -0
- super_dev/reviewers/code_review.py +471 -0
- super_dev/reviewers/quality_gate.py +964 -0
- super_dev/reviewers/redteam.py +881 -0
- super_dev/skills/__init__.py +7 -0
- super_dev/skills/manager.py +307 -0
- super_dev/specs/__init__.py +44 -0
- super_dev/specs/generator.py +264 -0
- super_dev/specs/manager.py +428 -0
- super_dev/specs/models.py +348 -0
- super_dev/specs/validator.py +415 -0
- super_dev/utils/__init__.py +11 -0
- super_dev/utils/logger.py +133 -0
- super_dev/web/api.py +1402 -0
- super_dev-2.0.0.dist-info/METADATA +252 -0
- super_dev-2.0.0.dist-info/RECORD +61 -0
- super_dev-2.0.0.dist-info/WHEEL +5 -0
- super_dev-2.0.0.dist-info/entry_points.txt +2 -0
- super_dev-2.0.0.dist-info/licenses/LICENSE +21 -0
- super_dev-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
"""
|
|
2
|
+
开发:Excellent(11964948@qq.com)
|
|
3
|
+
功能:工作流编排引擎 - 协调 12 阶段工作流(含第 0 阶段)
|
|
4
|
+
作用:管理任务执行、专家调度、质量门禁
|
|
5
|
+
创建时间:2025-12-30
|
|
6
|
+
最后修改:2026-02-24
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import traceback
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
RICH_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
RICH_AVAILABLE = False
|
|
26
|
+
|
|
27
|
+
from ..config.manager import ConfigManager, get_config_manager
|
|
28
|
+
from ..exceptions import PhaseExecutionError, QualityGateError
|
|
29
|
+
from ..utils import get_logger
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Phase(Enum):
|
|
33
|
+
"""工作流阶段"""
|
|
34
|
+
DISCOVERY = "discovery"
|
|
35
|
+
INTELLIGENCE = "intelligence"
|
|
36
|
+
DRAFTING = "drafting"
|
|
37
|
+
REDTEAM = "redteam"
|
|
38
|
+
QA = "qa"
|
|
39
|
+
DELIVERY = "delivery"
|
|
40
|
+
DEPLOYMENT = "deployment"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class PhaseResult:
|
|
45
|
+
"""阶段执行结果"""
|
|
46
|
+
phase: Phase
|
|
47
|
+
success: bool
|
|
48
|
+
duration: float
|
|
49
|
+
output: Any = None
|
|
50
|
+
errors: list = field(default_factory=list)
|
|
51
|
+
quality_score: float = 0.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class WorkflowContext:
|
|
56
|
+
"""工作流上下文"""
|
|
57
|
+
project_dir: Path
|
|
58
|
+
config: ConfigManager
|
|
59
|
+
results: dict = field(default_factory=dict)
|
|
60
|
+
metadata: dict = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
# 共享数据
|
|
63
|
+
user_input: dict = field(default_factory=dict)
|
|
64
|
+
research_data: dict = field(default_factory=dict)
|
|
65
|
+
documents: dict = field(default_factory=dict)
|
|
66
|
+
quality_reports: dict = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class WorkflowEngine:
|
|
70
|
+
"""工作流编排引擎"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, project_dir: Path | None = None):
|
|
73
|
+
self.project_dir = Path.cwd() if project_dir is None else project_dir
|
|
74
|
+
self.config_manager = get_config_manager(self.project_dir)
|
|
75
|
+
self.console = Console() if RICH_AVAILABLE else None
|
|
76
|
+
self.logger = get_logger(
|
|
77
|
+
'workflow_engine',
|
|
78
|
+
level='INFO',
|
|
79
|
+
log_file=self.project_dir / 'logs' / 'workflow.log'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 阶段注册表
|
|
83
|
+
self._phase_handlers: dict[Phase, Callable] = {}
|
|
84
|
+
|
|
85
|
+
# 注册默认阶段处理器
|
|
86
|
+
self._register_default_handlers()
|
|
87
|
+
|
|
88
|
+
self.logger.info("工作流引擎初始化完成", extra={'project_dir': str(self.project_dir)})
|
|
89
|
+
|
|
90
|
+
def _register_default_handlers(self) -> None:
|
|
91
|
+
"""注册默认阶段处理器"""
|
|
92
|
+
self._phase_handlers[Phase.DISCOVERY] = self._phase_discovery
|
|
93
|
+
self._phase_handlers[Phase.INTELLIGENCE] = self._phase_intelligence
|
|
94
|
+
self._phase_handlers[Phase.DRAFTING] = self._phase_drafting
|
|
95
|
+
self._phase_handlers[Phase.REDTEAM] = self._phase_redteam
|
|
96
|
+
self._phase_handlers[Phase.QA] = self._phase_qa
|
|
97
|
+
self._phase_handlers[Phase.DELIVERY] = self._phase_delivery
|
|
98
|
+
self._phase_handlers[Phase.DEPLOYMENT] = self._phase_deployment
|
|
99
|
+
|
|
100
|
+
def register_phase_handler(self, phase: Phase, handler: Callable) -> None:
|
|
101
|
+
self._phase_handlers[phase] = handler
|
|
102
|
+
|
|
103
|
+
async def run(
|
|
104
|
+
self,
|
|
105
|
+
phases: list[Phase] | None = None,
|
|
106
|
+
context: WorkflowContext | None = None,
|
|
107
|
+
stop_requested: Callable[[], bool] | None = None,
|
|
108
|
+
) -> dict[Phase, PhaseResult]:
|
|
109
|
+
if context is None:
|
|
110
|
+
context = WorkflowContext(
|
|
111
|
+
project_dir=self.project_dir,
|
|
112
|
+
config=self.config_manager
|
|
113
|
+
)
|
|
114
|
+
self._seed_context_user_input(context)
|
|
115
|
+
|
|
116
|
+
if phases is None:
|
|
117
|
+
phases = self._get_phases_from_config()
|
|
118
|
+
|
|
119
|
+
results = {}
|
|
120
|
+
self._print_workflow_start(phases)
|
|
121
|
+
|
|
122
|
+
for phase in phases:
|
|
123
|
+
if stop_requested and stop_requested():
|
|
124
|
+
self.logger.warning(
|
|
125
|
+
f"工作流收到停止请求,在阶段 {phase.value} 开始前终止",
|
|
126
|
+
extra={"phase": phase.value}
|
|
127
|
+
)
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
result = await self._run_phase(phase, context)
|
|
132
|
+
results[phase] = result
|
|
133
|
+
|
|
134
|
+
# 红队审查必须通过,避免风险进入后续阶段
|
|
135
|
+
if phase == Phase.REDTEAM and isinstance(result.output, dict):
|
|
136
|
+
if not bool(result.output.get("passed", True)):
|
|
137
|
+
reasons = result.output.get("blocking_reasons") or ["红队审查未通过"]
|
|
138
|
+
reason_text = "; ".join(str(r) for r in reasons)
|
|
139
|
+
quality_error = QualityGateError(
|
|
140
|
+
score=float(result.output.get("score", result.quality_score)),
|
|
141
|
+
threshold=float(result.output.get("pass_threshold", 70)),
|
|
142
|
+
details={"phase": phase.value, "reasons": reasons},
|
|
143
|
+
)
|
|
144
|
+
result.success = False
|
|
145
|
+
result.errors.append(reason_text)
|
|
146
|
+
raise quality_error
|
|
147
|
+
|
|
148
|
+
# 质量门禁只在 QA 阶段执行,避免前置阶段被全局阈值误杀
|
|
149
|
+
if phase == Phase.QA and result.quality_score < self.config_manager.config.quality_gate:
|
|
150
|
+
self._print_quality_gate_failed(phase, result)
|
|
151
|
+
quality_error = QualityGateError(
|
|
152
|
+
score=result.quality_score,
|
|
153
|
+
threshold=self.config_manager.config.quality_gate,
|
|
154
|
+
details={'phase': phase.value}
|
|
155
|
+
)
|
|
156
|
+
result.success = False
|
|
157
|
+
result.errors.append(str(quality_error))
|
|
158
|
+
raise quality_error
|
|
159
|
+
|
|
160
|
+
self._print_phase_complete(phase, result)
|
|
161
|
+
|
|
162
|
+
except QualityGateError as e:
|
|
163
|
+
self.logger.error(
|
|
164
|
+
f"工作流在阶段 {phase.value} 因质量门禁终止",
|
|
165
|
+
extra={'error': str(e), 'phase': phase.value}
|
|
166
|
+
)
|
|
167
|
+
if phase in results:
|
|
168
|
+
results[phase].success = False
|
|
169
|
+
if str(e) not in results[phase].errors:
|
|
170
|
+
results[phase].errors.append(str(e))
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
except PhaseExecutionError as e:
|
|
174
|
+
self.logger.error(
|
|
175
|
+
f"工作流在阶段 {phase.value} 终止",
|
|
176
|
+
extra={'error': str(e), 'phase': phase.value}
|
|
177
|
+
)
|
|
178
|
+
results[phase] = PhaseResult(
|
|
179
|
+
phase=phase,
|
|
180
|
+
success=False,
|
|
181
|
+
duration=0.0,
|
|
182
|
+
errors=[str(e)]
|
|
183
|
+
)
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
self._print_workflow_complete(results)
|
|
187
|
+
self._save_report(results)
|
|
188
|
+
return results
|
|
189
|
+
|
|
190
|
+
def _get_phases_from_config(self) -> list[Phase]:
|
|
191
|
+
config_phases = self.config_manager.config.phases
|
|
192
|
+
phases = []
|
|
193
|
+
phase_map = {
|
|
194
|
+
"discovery": Phase.DISCOVERY,
|
|
195
|
+
"intelligence": Phase.INTELLIGENCE,
|
|
196
|
+
"drafting": Phase.DRAFTING,
|
|
197
|
+
"redteam": Phase.REDTEAM,
|
|
198
|
+
"qa": Phase.QA,
|
|
199
|
+
"delivery": Phase.DELIVERY,
|
|
200
|
+
"deployment": Phase.DEPLOYMENT,
|
|
201
|
+
}
|
|
202
|
+
for p in config_phases:
|
|
203
|
+
if p in phase_map:
|
|
204
|
+
phases.append(phase_map[p])
|
|
205
|
+
return phases
|
|
206
|
+
|
|
207
|
+
async def _run_phase(self, phase: Phase, context: WorkflowContext) -> PhaseResult:
|
|
208
|
+
start_time = datetime.now()
|
|
209
|
+
phase_name = phase.value.upper()
|
|
210
|
+
|
|
211
|
+
self.logger.info(f"开始执行阶段: {phase_name}", extra={'phase': phase_name})
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
handler = self._phase_handlers.get(phase)
|
|
215
|
+
if handler is None:
|
|
216
|
+
raise PhaseExecutionError(
|
|
217
|
+
phase=phase_name,
|
|
218
|
+
message=f"No handler registered for phase: {phase}",
|
|
219
|
+
details={'available_phases': list(self._phase_handlers.keys())}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
output = await self._execute_handler(handler, context)
|
|
223
|
+
duration = (datetime.now() - start_time).total_seconds()
|
|
224
|
+
|
|
225
|
+
# 兼容自定义处理器直接返回 PhaseResult 的场景
|
|
226
|
+
if isinstance(output, PhaseResult):
|
|
227
|
+
result = output
|
|
228
|
+
if result.phase != phase:
|
|
229
|
+
result.phase = phase
|
|
230
|
+
if result.duration <= 0:
|
|
231
|
+
result.duration = duration
|
|
232
|
+
if result.quality_score <= 0:
|
|
233
|
+
result.quality_score = self._calculate_quality_score(phase, context)
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
quality_score = self._calculate_quality_score(phase, context)
|
|
237
|
+
|
|
238
|
+
self.logger.info(
|
|
239
|
+
f"阶段执行成功: {phase_name}",
|
|
240
|
+
extra={
|
|
241
|
+
'phase': phase_name,
|
|
242
|
+
'duration': duration,
|
|
243
|
+
'quality_score': quality_score
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return PhaseResult(
|
|
248
|
+
phase=phase,
|
|
249
|
+
success=True,
|
|
250
|
+
duration=duration,
|
|
251
|
+
output=output,
|
|
252
|
+
quality_score=quality_score
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
except PhaseExecutionError:
|
|
256
|
+
raise
|
|
257
|
+
except QualityGateError:
|
|
258
|
+
raise
|
|
259
|
+
except Exception as e:
|
|
260
|
+
duration = (datetime.now() - start_time).total_seconds()
|
|
261
|
+
error_details = {
|
|
262
|
+
'error_type': type(e).__name__,
|
|
263
|
+
'error_message': str(e),
|
|
264
|
+
'traceback': traceback.format_exc(),
|
|
265
|
+
'phase': phase_name,
|
|
266
|
+
'duration': duration
|
|
267
|
+
}
|
|
268
|
+
self.logger.error(f"阶段执行失败: {phase_name}", extra=error_details)
|
|
269
|
+
raise PhaseExecutionError(
|
|
270
|
+
phase=phase_name,
|
|
271
|
+
message=f"Phase execution failed: {str(e)}",
|
|
272
|
+
details=error_details
|
|
273
|
+
) from e
|
|
274
|
+
|
|
275
|
+
async def _execute_handler(self, handler: Callable, context: WorkflowContext) -> Any:
|
|
276
|
+
if asyncio.iscoroutinefunction(handler):
|
|
277
|
+
return await handler(context)
|
|
278
|
+
else:
|
|
279
|
+
return handler(context)
|
|
280
|
+
|
|
281
|
+
def _calculate_quality_score(self, phase: Phase, context: WorkflowContext) -> float:
|
|
282
|
+
"""使用真实质量评分引擎计算分数"""
|
|
283
|
+
try:
|
|
284
|
+
from .quality import QualityScorer
|
|
285
|
+
configured_name = "project"
|
|
286
|
+
config_obj = getattr(context, "config", None)
|
|
287
|
+
if config_obj is not None:
|
|
288
|
+
# 兼容 WorkflowContext.config 为 ConfigManager 或 ProjectConfig 两种形态
|
|
289
|
+
manager_cfg = getattr(config_obj, "config", None)
|
|
290
|
+
if manager_cfg is not None:
|
|
291
|
+
configured_name = getattr(manager_cfg, "name", None) or configured_name
|
|
292
|
+
else:
|
|
293
|
+
configured_name = getattr(config_obj, "name", None) or configured_name
|
|
294
|
+
|
|
295
|
+
name = str(context.user_input.get("name", configured_name))
|
|
296
|
+
scorer = QualityScorer(project_dir=self.project_dir, name=name)
|
|
297
|
+
score = scorer.score_phase(
|
|
298
|
+
phase_name=phase.value,
|
|
299
|
+
context_data=context.user_input,
|
|
300
|
+
)
|
|
301
|
+
return float(score)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
self.logger.warning(f"质量评分失败,使用默认值: {e}")
|
|
304
|
+
return 75.0
|
|
305
|
+
|
|
306
|
+
# ==================== 阶段处理器(真实实现)====================
|
|
307
|
+
|
|
308
|
+
async def _phase_discovery(self, context: WorkflowContext) -> Any:
|
|
309
|
+
"""
|
|
310
|
+
第 0 阶段:需求增强
|
|
311
|
+
- 解析用户输入的自然语言需求
|
|
312
|
+
- 注入领域知识库
|
|
313
|
+
- 联网检索补充背景信息(可选)
|
|
314
|
+
"""
|
|
315
|
+
from ..creators.requirement_parser import RequirementParser
|
|
316
|
+
from .knowledge import KnowledgeAugmenter
|
|
317
|
+
|
|
318
|
+
user_input = context.user_input
|
|
319
|
+
description = user_input.get("enriched_description", user_input.get("description", ""))
|
|
320
|
+
domain = user_input.get("domain", "")
|
|
321
|
+
offline = user_input.get("offline", False)
|
|
322
|
+
|
|
323
|
+
# 1. 解析结构化需求
|
|
324
|
+
parser = RequirementParser()
|
|
325
|
+
requirements = parser.parse_requirements(description)
|
|
326
|
+
scenario = parser.detect_scenario(self.project_dir)
|
|
327
|
+
context.user_input["scenario"] = scenario
|
|
328
|
+
context.user_input["requirements"] = requirements
|
|
329
|
+
|
|
330
|
+
# 2. 需求知识增强(本地知识库 + 联网检索)
|
|
331
|
+
try:
|
|
332
|
+
augmenter = KnowledgeAugmenter(project_dir=self.project_dir, web_enabled=not offline)
|
|
333
|
+
knowledge_bundle = augmenter.augment(requirement=description, domain=domain)
|
|
334
|
+
context.research_data["knowledge_bundle"] = knowledge_bundle
|
|
335
|
+
context.user_input["knowledge_enhanced"] = bool(knowledge_bundle.get("local_knowledge"))
|
|
336
|
+
context.user_input["web_research"] = bool(knowledge_bundle.get("web_knowledge"))
|
|
337
|
+
context.user_input["enriched_description"] = knowledge_bundle.get("enriched_requirement", description)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
self.logger.warning(f"需求知识增强跳过: {e}")
|
|
340
|
+
context.user_input["knowledge_enhanced"] = False
|
|
341
|
+
context.user_input["web_research"] = False
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
"status": "discovery_complete",
|
|
345
|
+
"scenario": scenario,
|
|
346
|
+
"requirements_count": len(requirements),
|
|
347
|
+
"knowledge_enhanced": context.user_input.get("knowledge_enhanced"),
|
|
348
|
+
"web_research": context.user_input.get("web_research"),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async def _phase_intelligence(self, context: WorkflowContext) -> Any:
|
|
352
|
+
"""
|
|
353
|
+
第 0.5 阶段:市场情报
|
|
354
|
+
- 基于需求进行深度联网检索
|
|
355
|
+
- 竞品分析
|
|
356
|
+
- 技术趋势调研
|
|
357
|
+
"""
|
|
358
|
+
description = context.user_input.get(
|
|
359
|
+
"enriched_description",
|
|
360
|
+
context.user_input.get("description", ""),
|
|
361
|
+
)
|
|
362
|
+
offline = context.user_input.get("offline", False)
|
|
363
|
+
|
|
364
|
+
intelligence: dict[str, list[dict[str, str]]] = {
|
|
365
|
+
"trends": [],
|
|
366
|
+
"competitors": [],
|
|
367
|
+
"best_practices": [],
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if not offline and description:
|
|
371
|
+
try:
|
|
372
|
+
# 技术趋势检索
|
|
373
|
+
trend_results = await self._web_search(f"{description} best practices 2024 2025")
|
|
374
|
+
if trend_results:
|
|
375
|
+
intelligence["trends"] = trend_results[:3]
|
|
376
|
+
|
|
377
|
+
# 竞品检索
|
|
378
|
+
competitor_results = await self._web_search(f"{description} top alternatives tools")
|
|
379
|
+
if competitor_results:
|
|
380
|
+
intelligence["competitors"] = competitor_results[:3]
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
self.logger.info(f"市场情报检索跳过: {e}")
|
|
384
|
+
|
|
385
|
+
context.research_data["intelligence"] = intelligence
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
"status": "intelligence_complete",
|
|
389
|
+
"trends_count": len(intelligence["trends"]),
|
|
390
|
+
"competitors_count": len(intelligence["competitors"]),
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async def _phase_drafting(self, context: WorkflowContext) -> Any:
|
|
394
|
+
"""
|
|
395
|
+
第 1 阶段:专家协作生成文档
|
|
396
|
+
- PM 专家:生成 PRD
|
|
397
|
+
- ARCHITECT 专家:生成架构文档
|
|
398
|
+
- UI/UX 专家:生成 UI/UX 文档
|
|
399
|
+
"""
|
|
400
|
+
from .experts import ExpertDispatcher
|
|
401
|
+
|
|
402
|
+
user_input = context.user_input
|
|
403
|
+
name = user_input.get("name", self.config_manager.config.name or "project")
|
|
404
|
+
description = user_input.get("enriched_description", user_input.get("description", ""))
|
|
405
|
+
platform = user_input.get("platform", "web")
|
|
406
|
+
frontend = user_input.get("frontend", "react")
|
|
407
|
+
backend = user_input.get("backend", "node")
|
|
408
|
+
domain = user_input.get("domain", "")
|
|
409
|
+
|
|
410
|
+
dispatcher = ExpertDispatcher(self.project_dir)
|
|
411
|
+
result = dispatcher.dispatch_document_generation(
|
|
412
|
+
name=name,
|
|
413
|
+
description=description,
|
|
414
|
+
platform=platform,
|
|
415
|
+
frontend=frontend,
|
|
416
|
+
backend=backend,
|
|
417
|
+
domain=domain,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# 保存生成的文档到 output/ 目录
|
|
421
|
+
output_dir = self.project_dir / "output"
|
|
422
|
+
output_dir.mkdir(exist_ok=True)
|
|
423
|
+
|
|
424
|
+
saved_docs = []
|
|
425
|
+
for expert_output in result.outputs:
|
|
426
|
+
doc_path = output_dir / f"{name}-{expert_output.document_type}.md"
|
|
427
|
+
doc_path.write_text(expert_output.content, encoding="utf-8")
|
|
428
|
+
saved_docs.append(str(doc_path))
|
|
429
|
+
context.documents[expert_output.document_type] = expert_output.content
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
"status": "documents_generated",
|
|
433
|
+
"documents": saved_docs,
|
|
434
|
+
"team_score": result.total_score,
|
|
435
|
+
"summary": result.summary,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async def _phase_redteam(self, context: WorkflowContext) -> Any:
|
|
439
|
+
"""
|
|
440
|
+
第 5 阶段:红队审查
|
|
441
|
+
- SECURITY 专家:安全审查(注入、XSS、CSRF 等)
|
|
442
|
+
- 性能审查(N+1、缓存、分页)
|
|
443
|
+
- 架构审查(可扩展性、可维护性)
|
|
444
|
+
"""
|
|
445
|
+
from .experts import ExpertDispatcher
|
|
446
|
+
|
|
447
|
+
user_input = context.user_input
|
|
448
|
+
name = user_input.get("name", self.config_manager.config.name or "project")
|
|
449
|
+
tech_stack = {
|
|
450
|
+
"platform": user_input.get("platform", "web"),
|
|
451
|
+
"frontend": user_input.get("frontend", "react"),
|
|
452
|
+
"backend": user_input.get("backend", "node"),
|
|
453
|
+
"domain": user_input.get("domain", ""),
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
dispatcher = ExpertDispatcher(self.project_dir)
|
|
457
|
+
expert_output = dispatcher.dispatch_redteam_review(name=name, tech_stack=tech_stack)
|
|
458
|
+
|
|
459
|
+
# 保存红队报告
|
|
460
|
+
output_dir = self.project_dir / "output"
|
|
461
|
+
output_dir.mkdir(exist_ok=True)
|
|
462
|
+
redteam_path = output_dir / f"{name}-redteam.md"
|
|
463
|
+
redteam_path.write_text(expert_output.content, encoding="utf-8")
|
|
464
|
+
|
|
465
|
+
# 保存报告到上下文(供 QA 阶段使用)
|
|
466
|
+
context.quality_reports["redteam"] = expert_output.metadata
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
"status": "redteam_complete",
|
|
470
|
+
"report_path": str(redteam_path),
|
|
471
|
+
"score": expert_output.quality_score,
|
|
472
|
+
"passed": expert_output.metadata.get("passed", False),
|
|
473
|
+
"pass_threshold": expert_output.metadata.get("pass_threshold", 70),
|
|
474
|
+
"blocking_reasons": expert_output.metadata.get("blocking_reasons", []),
|
|
475
|
+
"critical_count": expert_output.metadata.get("critical_count", 0),
|
|
476
|
+
"high_count": expert_output.metadata.get("high_count", 0),
|
|
477
|
+
"issues": {
|
|
478
|
+
"security": expert_output.metadata.get("security_issues", []),
|
|
479
|
+
"performance": expert_output.metadata.get("performance_issues", []),
|
|
480
|
+
"architecture": expert_output.metadata.get("architecture_issues", []),
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async def _phase_qa(self, context: WorkflowContext) -> Any:
|
|
485
|
+
"""
|
|
486
|
+
第 6 阶段:质量门禁
|
|
487
|
+
- QA 专家:多维度质量门禁检查
|
|
488
|
+
- 场景化检查但统一 80+ 通过标准(支持手动覆盖)
|
|
489
|
+
- 生成详细质量报告
|
|
490
|
+
"""
|
|
491
|
+
from ..reviewers.redteam import RedTeamReviewer
|
|
492
|
+
from .experts import ExpertDispatcher
|
|
493
|
+
|
|
494
|
+
user_input = context.user_input
|
|
495
|
+
name = user_input.get("name", self.config_manager.config.name or "project")
|
|
496
|
+
tech_stack = {
|
|
497
|
+
"platform": user_input.get("platform", "web"),
|
|
498
|
+
"frontend": user_input.get("frontend", "react"),
|
|
499
|
+
"backend": user_input.get("backend", "node"),
|
|
500
|
+
"domain": user_input.get("domain", ""),
|
|
501
|
+
}
|
|
502
|
+
threshold_override = user_input.get("quality_threshold")
|
|
503
|
+
|
|
504
|
+
# 重新加载红队报告(如果存在)
|
|
505
|
+
redteam_report = None
|
|
506
|
+
try:
|
|
507
|
+
reviewer = RedTeamReviewer(
|
|
508
|
+
project_dir=self.project_dir,
|
|
509
|
+
name=name,
|
|
510
|
+
tech_stack=tech_stack,
|
|
511
|
+
)
|
|
512
|
+
redteam_report = reviewer.review()
|
|
513
|
+
except Exception as e:
|
|
514
|
+
self.logger.warning(f"红队报告加载失败,跳过: {e}")
|
|
515
|
+
|
|
516
|
+
dispatcher = ExpertDispatcher(self.project_dir)
|
|
517
|
+
expert_output = dispatcher.dispatch_quality_gate(
|
|
518
|
+
name=name,
|
|
519
|
+
tech_stack=tech_stack,
|
|
520
|
+
redteam_report=redteam_report,
|
|
521
|
+
threshold_override=threshold_override,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# 保存质量门禁报告
|
|
525
|
+
output_dir = self.project_dir / "output"
|
|
526
|
+
output_dir.mkdir(exist_ok=True)
|
|
527
|
+
qg_path = output_dir / f"{name}-quality-gate.md"
|
|
528
|
+
qg_path.write_text(expert_output.content, encoding="utf-8")
|
|
529
|
+
|
|
530
|
+
passed = expert_output.metadata.get("passed", False)
|
|
531
|
+
if not passed:
|
|
532
|
+
raise QualityGateError(
|
|
533
|
+
score=expert_output.quality_score,
|
|
534
|
+
threshold=threshold_override or 80,
|
|
535
|
+
details={"phase": "qa", "report_path": str(qg_path)}
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
"status": "quality_gate_passed",
|
|
540
|
+
"score": expert_output.quality_score,
|
|
541
|
+
"scenario": expert_output.metadata.get("scenario"),
|
|
542
|
+
"report_path": str(qg_path),
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async def _phase_delivery(self, context: WorkflowContext) -> Any:
|
|
546
|
+
"""
|
|
547
|
+
第 7-8 阶段:代码审查指南 + AI 提示词生成
|
|
548
|
+
- CODE 专家:代码审查指南
|
|
549
|
+
- CODE 专家:AI 提示词(用户复制给 AI 即可开始开发)
|
|
550
|
+
"""
|
|
551
|
+
from .experts import ExpertDispatcher
|
|
552
|
+
|
|
553
|
+
user_input = context.user_input
|
|
554
|
+
name = user_input.get("name", self.config_manager.config.name or "project")
|
|
555
|
+
tech_stack = {
|
|
556
|
+
"platform": user_input.get("platform", "web"),
|
|
557
|
+
"frontend": user_input.get("frontend", "react"),
|
|
558
|
+
"backend": user_input.get("backend", "node"),
|
|
559
|
+
"domain": user_input.get("domain", ""),
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
dispatcher = ExpertDispatcher(self.project_dir)
|
|
563
|
+
output_dir = self.project_dir / "output"
|
|
564
|
+
output_dir.mkdir(exist_ok=True)
|
|
565
|
+
saved = []
|
|
566
|
+
step_errors: list[str] = []
|
|
567
|
+
|
|
568
|
+
# 代码审查指南
|
|
569
|
+
try:
|
|
570
|
+
cr_output = dispatcher.dispatch_code_review(name=name, tech_stack=tech_stack)
|
|
571
|
+
cr_path = output_dir / f"{name}-code-review.md"
|
|
572
|
+
cr_path.write_text(cr_output.content, encoding="utf-8")
|
|
573
|
+
saved.append(str(cr_path))
|
|
574
|
+
except Exception as e:
|
|
575
|
+
msg = f"代码审查指南生成失败: {e}"
|
|
576
|
+
self.logger.error(msg)
|
|
577
|
+
step_errors.append(msg)
|
|
578
|
+
|
|
579
|
+
# AI 提示词
|
|
580
|
+
try:
|
|
581
|
+
ai_output = dispatcher.dispatch_ai_prompt(name=name)
|
|
582
|
+
ai_path = output_dir / f"{name}-ai-prompt.md"
|
|
583
|
+
ai_path.write_text(ai_output.content, encoding="utf-8")
|
|
584
|
+
saved.append(str(ai_path))
|
|
585
|
+
except Exception as e:
|
|
586
|
+
msg = f"AI 提示词生成失败: {e}"
|
|
587
|
+
self.logger.error(msg)
|
|
588
|
+
step_errors.append(msg)
|
|
589
|
+
|
|
590
|
+
if step_errors:
|
|
591
|
+
raise PhaseExecutionError(
|
|
592
|
+
phase=Phase.DELIVERY.value.upper(),
|
|
593
|
+
message="Delivery phase failed",
|
|
594
|
+
details={"errors": step_errors},
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
"status": "delivery_complete",
|
|
599
|
+
"files": saved,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async def _phase_deployment(self, context: WorkflowContext) -> Any:
|
|
603
|
+
"""
|
|
604
|
+
第 9-11 阶段:CI/CD 配置 + 数据库迁移 + 交付包
|
|
605
|
+
- DEVOPS 专家:生成 5 大 CI/CD 平台配置
|
|
606
|
+
- DBA 专家:生成 6 种 ORM 数据库迁移脚本
|
|
607
|
+
- 交付收敛:生成 manifest/report/zip 交付包
|
|
608
|
+
"""
|
|
609
|
+
from .. import __version__
|
|
610
|
+
from ..deployers import DeliveryPackager
|
|
611
|
+
from .experts import ExpertDispatcher
|
|
612
|
+
|
|
613
|
+
user_input = context.user_input
|
|
614
|
+
name = user_input.get("name", self.config_manager.config.name or "project")
|
|
615
|
+
tech_stack = {
|
|
616
|
+
"platform": user_input.get("platform", "web"),
|
|
617
|
+
"frontend": user_input.get("frontend", "react"),
|
|
618
|
+
"backend": user_input.get("backend", "node"),
|
|
619
|
+
"domain": user_input.get("domain", ""),
|
|
620
|
+
}
|
|
621
|
+
cicd_platform = user_input.get("cicd", "github")
|
|
622
|
+
orm = user_input.get("orm", self._detect_orm())
|
|
623
|
+
|
|
624
|
+
dispatcher = ExpertDispatcher(self.project_dir)
|
|
625
|
+
output_dir = self.project_dir / "output"
|
|
626
|
+
output_dir.mkdir(exist_ok=True)
|
|
627
|
+
saved = []
|
|
628
|
+
step_errors: list[str] = []
|
|
629
|
+
|
|
630
|
+
# CI/CD 配置
|
|
631
|
+
try:
|
|
632
|
+
cicd_output = dispatcher.dispatch_cicd(
|
|
633
|
+
name=name,
|
|
634
|
+
tech_stack=tech_stack,
|
|
635
|
+
cicd_platform=cicd_platform,
|
|
636
|
+
)
|
|
637
|
+
cicd_path = output_dir / f"{name}-cicd.md"
|
|
638
|
+
cicd_path.write_text(cicd_output.content, encoding="utf-8")
|
|
639
|
+
saved.append(str(cicd_path))
|
|
640
|
+
generated = cicd_output.metadata.get("generated_file_contents", {})
|
|
641
|
+
saved.extend(self._write_generated_files(generated))
|
|
642
|
+
except Exception as e:
|
|
643
|
+
msg = f"CI/CD 配置生成失败: {e}"
|
|
644
|
+
self.logger.error(msg)
|
|
645
|
+
step_errors.append(msg)
|
|
646
|
+
|
|
647
|
+
# 数据库迁移
|
|
648
|
+
try:
|
|
649
|
+
migration_output = dispatcher.dispatch_migration(
|
|
650
|
+
name=name,
|
|
651
|
+
tech_stack=tech_stack,
|
|
652
|
+
orm=orm,
|
|
653
|
+
)
|
|
654
|
+
migration_path = output_dir / f"{name}-migration.md"
|
|
655
|
+
migration_path.write_text(migration_output.content, encoding="utf-8")
|
|
656
|
+
saved.append(str(migration_path))
|
|
657
|
+
generated = migration_output.metadata.get("generated_file_contents", {})
|
|
658
|
+
saved.extend(self._write_generated_files(generated))
|
|
659
|
+
except Exception as e:
|
|
660
|
+
msg = f"数据库迁移脚本生成失败: {e}"
|
|
661
|
+
self.logger.error(msg)
|
|
662
|
+
step_errors.append(msg)
|
|
663
|
+
|
|
664
|
+
# 交付包
|
|
665
|
+
try:
|
|
666
|
+
packager = DeliveryPackager(
|
|
667
|
+
project_dir=self.project_dir,
|
|
668
|
+
name=name,
|
|
669
|
+
version=__version__,
|
|
670
|
+
)
|
|
671
|
+
delivery_outputs = packager.package(cicd_platform=cicd_platform)
|
|
672
|
+
saved.append(str(delivery_outputs["manifest_file"]))
|
|
673
|
+
saved.append(str(delivery_outputs["report_file"]))
|
|
674
|
+
saved.append(str(delivery_outputs["archive_file"]))
|
|
675
|
+
except Exception as e:
|
|
676
|
+
msg = f"交付包生成失败: {e}"
|
|
677
|
+
self.logger.error(msg)
|
|
678
|
+
step_errors.append(msg)
|
|
679
|
+
|
|
680
|
+
if step_errors:
|
|
681
|
+
raise PhaseExecutionError(
|
|
682
|
+
phase=Phase.DEPLOYMENT.value.upper(),
|
|
683
|
+
message="Deployment phase failed",
|
|
684
|
+
details={"errors": step_errors},
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
"status": "deployment_ready",
|
|
689
|
+
"cicd_platform": cicd_platform,
|
|
690
|
+
"orm": orm,
|
|
691
|
+
"files": saved,
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
# ==================== 联网检索 ====================
|
|
695
|
+
|
|
696
|
+
async def _web_search(self, query: str, max_results: int = 5) -> list[dict]:
|
|
697
|
+
"""
|
|
698
|
+
联网检索(优先使用 DuckDuckGo,自动降级到离线模式)
|
|
699
|
+
|
|
700
|
+
中国大陆环境:DuckDuckGo API 通常可访问;
|
|
701
|
+
如需国内可访问的备选,可配置 TAVILY_API_KEY。
|
|
702
|
+
"""
|
|
703
|
+
# 优先尝试 Tavily(如配置了 API Key)
|
|
704
|
+
import os
|
|
705
|
+
tavily_key = os.environ.get("TAVILY_API_KEY") or os.environ.get("SUPER_DEV_TAVILY_KEY")
|
|
706
|
+
if tavily_key:
|
|
707
|
+
try:
|
|
708
|
+
return await self._search_tavily(query, tavily_key, max_results)
|
|
709
|
+
except Exception as e:
|
|
710
|
+
self.logger.debug(f"Tavily 检索失败,降级到 DuckDuckGo: {e}")
|
|
711
|
+
|
|
712
|
+
# 降级到 DuckDuckGo(无需 API Key)
|
|
713
|
+
try:
|
|
714
|
+
return await self._search_duckduckgo(query, max_results)
|
|
715
|
+
except Exception as e:
|
|
716
|
+
self.logger.debug(f"DuckDuckGo 检索失败,进入完全离线模式: {e}")
|
|
717
|
+
return []
|
|
718
|
+
|
|
719
|
+
async def _search_duckduckgo(self, query: str, max_results: int = 5) -> list[dict]:
|
|
720
|
+
"""使用 DuckDuckGo Instant Answer API 检索"""
|
|
721
|
+
import urllib.parse
|
|
722
|
+
import urllib.request
|
|
723
|
+
|
|
724
|
+
encoded_query = urllib.parse.quote(query)
|
|
725
|
+
url = f"https://api.duckduckgo.com/?q={encoded_query}&format=json&no_html=1&skip_disambig=1"
|
|
726
|
+
|
|
727
|
+
# 在线程池中执行阻塞 IO
|
|
728
|
+
loop = asyncio.get_event_loop()
|
|
729
|
+
response_text = await loop.run_in_executor(
|
|
730
|
+
None,
|
|
731
|
+
lambda: self._http_get(url, timeout=5)
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
if not response_text:
|
|
735
|
+
return []
|
|
736
|
+
|
|
737
|
+
data = json.loads(response_text)
|
|
738
|
+
results = []
|
|
739
|
+
|
|
740
|
+
# 摘要结果
|
|
741
|
+
if data.get("Abstract"):
|
|
742
|
+
results.append({
|
|
743
|
+
"title": data.get("Heading", query),
|
|
744
|
+
"snippet": data.get("Abstract", ""),
|
|
745
|
+
"url": data.get("AbstractURL", ""),
|
|
746
|
+
"source": "DuckDuckGo",
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
# 相关主题
|
|
750
|
+
for topic in data.get("RelatedTopics", [])[:max_results - 1]:
|
|
751
|
+
if isinstance(topic, dict) and topic.get("Text"):
|
|
752
|
+
results.append({
|
|
753
|
+
"title": topic.get("Text", "")[:80],
|
|
754
|
+
"snippet": topic.get("Text", ""),
|
|
755
|
+
"url": topic.get("FirstURL", ""),
|
|
756
|
+
"source": "DuckDuckGo",
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
return results[:max_results]
|
|
760
|
+
|
|
761
|
+
async def _search_tavily(self, query: str, api_key: str, max_results: int = 5) -> list[dict]:
|
|
762
|
+
"""使用 Tavily API 进行深度检索"""
|
|
763
|
+
import json as json_mod
|
|
764
|
+
|
|
765
|
+
payload = json_mod.dumps({
|
|
766
|
+
"api_key": api_key,
|
|
767
|
+
"query": query,
|
|
768
|
+
"search_depth": "basic",
|
|
769
|
+
"max_results": max_results,
|
|
770
|
+
}).encode("utf-8")
|
|
771
|
+
|
|
772
|
+
loop = asyncio.get_event_loop()
|
|
773
|
+
response_text = await loop.run_in_executor(
|
|
774
|
+
None,
|
|
775
|
+
lambda: self._http_post(
|
|
776
|
+
"https://api.tavily.com/search",
|
|
777
|
+
payload,
|
|
778
|
+
headers={"Content-Type": "application/json"},
|
|
779
|
+
timeout=10,
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
if not response_text:
|
|
784
|
+
return []
|
|
785
|
+
|
|
786
|
+
data = json.loads(response_text)
|
|
787
|
+
results = []
|
|
788
|
+
for item in data.get("results", [])[:max_results]:
|
|
789
|
+
results.append({
|
|
790
|
+
"title": item.get("title", ""),
|
|
791
|
+
"snippet": item.get("content", "")[:300],
|
|
792
|
+
"url": item.get("url", ""),
|
|
793
|
+
"source": "Tavily",
|
|
794
|
+
})
|
|
795
|
+
return results
|
|
796
|
+
|
|
797
|
+
def _http_get(self, url: str, timeout: int = 5) -> str | None:
|
|
798
|
+
"""同步 HTTP GET(在 executor 中执行)"""
|
|
799
|
+
import urllib.request
|
|
800
|
+
from urllib.parse import urlparse
|
|
801
|
+
try:
|
|
802
|
+
parsed = urlparse(url)
|
|
803
|
+
if parsed.scheme not in {"http", "https"}:
|
|
804
|
+
return None
|
|
805
|
+
req = urllib.request.Request(url, headers={"User-Agent": "super-dev/1.0"})
|
|
806
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec B310
|
|
807
|
+
raw: bytes = resp.read()
|
|
808
|
+
decoded = raw.decode("utf-8", errors="ignore")
|
|
809
|
+
return str(decoded)
|
|
810
|
+
except Exception:
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
def _http_post(self, url: str, data: bytes, headers: dict, timeout: int = 10) -> str | None:
|
|
814
|
+
"""同步 HTTP POST(在 executor 中执行)"""
|
|
815
|
+
import urllib.request
|
|
816
|
+
from urllib.parse import urlparse
|
|
817
|
+
try:
|
|
818
|
+
parsed = urlparse(url)
|
|
819
|
+
if parsed.scheme not in {"http", "https"}:
|
|
820
|
+
return None
|
|
821
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
822
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec B310
|
|
823
|
+
raw: bytes = resp.read()
|
|
824
|
+
decoded = raw.decode("utf-8", errors="ignore")
|
|
825
|
+
return str(decoded)
|
|
826
|
+
except Exception:
|
|
827
|
+
return None
|
|
828
|
+
|
|
829
|
+
def _detect_orm(self) -> str:
|
|
830
|
+
"""自动检测项目使用的 ORM"""
|
|
831
|
+
if (self.project_dir / "prisma").exists():
|
|
832
|
+
return "prisma"
|
|
833
|
+
if (self.project_dir / "requirements.txt").exists():
|
|
834
|
+
req = (self.project_dir / "requirements.txt").read_text(encoding="utf-8", errors="ignore")
|
|
835
|
+
if "sqlalchemy" in req.lower():
|
|
836
|
+
return "sqlalchemy"
|
|
837
|
+
if "django" in req.lower():
|
|
838
|
+
return "django"
|
|
839
|
+
if (self.project_dir / "package.json").exists():
|
|
840
|
+
pkg = (self.project_dir / "package.json").read_text(encoding="utf-8", errors="ignore")
|
|
841
|
+
if "typeorm" in pkg.lower():
|
|
842
|
+
return "typeorm"
|
|
843
|
+
if "sequelize" in pkg.lower():
|
|
844
|
+
return "sequelize"
|
|
845
|
+
if "mongoose" in pkg.lower():
|
|
846
|
+
return "mongoose"
|
|
847
|
+
return "prisma"
|
|
848
|
+
|
|
849
|
+
# ==================== 打印方法 ====================
|
|
850
|
+
|
|
851
|
+
def _print_workflow_start(self, phases: list[Phase]) -> None:
|
|
852
|
+
if self.console:
|
|
853
|
+
self.console.print(Panel.fit(
|
|
854
|
+
f"[bold cyan]Super Dev 工作流引擎[/bold cyan]\n\n"
|
|
855
|
+
f"项目: {self.config_manager.config.name}\n"
|
|
856
|
+
f"阶段: {len(phases)} 个",
|
|
857
|
+
title="启动"
|
|
858
|
+
))
|
|
859
|
+
|
|
860
|
+
def _print_phase_complete(self, phase: Phase, result: PhaseResult) -> None:
|
|
861
|
+
if self.console:
|
|
862
|
+
self.console.print(
|
|
863
|
+
f"[green]✓[/green] {phase.value}: "
|
|
864
|
+
f"完成 ({result.duration:.1f}s, 质量分: {result.quality_score:.0f})"
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
def _print_phase_failed(self, phase: Phase, result: PhaseResult) -> None:
|
|
868
|
+
if self.console:
|
|
869
|
+
self.console.print(
|
|
870
|
+
f"[red]✗[/red] {phase.value}: "
|
|
871
|
+
f"失败 ({', '.join(result.errors)})"
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
def _print_quality_gate_failed(self, phase: Phase, result: PhaseResult) -> None:
|
|
875
|
+
if self.console:
|
|
876
|
+
gate = self.config_manager.config.quality_gate
|
|
877
|
+
self.console.print(
|
|
878
|
+
f"[yellow]⚠[/yellow] {phase.value}: "
|
|
879
|
+
f"质量分 ({result.quality_score:.0f}) 低于门禁 ({gate})"
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
def _print_workflow_complete(self, results: dict[Phase, PhaseResult]) -> None:
|
|
883
|
+
if self.console:
|
|
884
|
+
table = Table(title="工作流执行结果")
|
|
885
|
+
table.add_column("阶段", style="cyan")
|
|
886
|
+
table.add_column("状态", style="green")
|
|
887
|
+
table.add_column("耗时", style="yellow")
|
|
888
|
+
table.add_column("质量分", style="magenta")
|
|
889
|
+
|
|
890
|
+
total_duration = 0.0
|
|
891
|
+
success_count = 0
|
|
892
|
+
|
|
893
|
+
for phase, result in results.items():
|
|
894
|
+
status = "[green]成功[/green]" if result.success else "[red]失败[/red]"
|
|
895
|
+
duration = f"{result.duration:.1f}s"
|
|
896
|
+
quality = f"{result.quality_score:.0f}"
|
|
897
|
+
|
|
898
|
+
table.add_row(phase.value, status, duration, quality)
|
|
899
|
+
total_duration += result.duration
|
|
900
|
+
if result.success:
|
|
901
|
+
success_count += 1
|
|
902
|
+
|
|
903
|
+
self.console.print(table)
|
|
904
|
+
self.console.print(
|
|
905
|
+
f"\n总计: {success_count}/{len(results)} 成功, "
|
|
906
|
+
f"总耗时: {total_duration:.1f}s"
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
def _save_report(self, results: dict[Phase, PhaseResult]) -> None:
|
|
910
|
+
output_dir = self.project_dir / self.config_manager.config.output_dir
|
|
911
|
+
output_dir.mkdir(exist_ok=True)
|
|
912
|
+
|
|
913
|
+
report_path = output_dir / f"workflow_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
914
|
+
|
|
915
|
+
report_data = {
|
|
916
|
+
"timestamp": datetime.now().isoformat(),
|
|
917
|
+
"project": self.config_manager.config.name,
|
|
918
|
+
"results": {
|
|
919
|
+
phase.value: {
|
|
920
|
+
"success": result.success,
|
|
921
|
+
"duration": result.duration,
|
|
922
|
+
"quality_score": result.quality_score,
|
|
923
|
+
"errors": result.errors
|
|
924
|
+
}
|
|
925
|
+
for phase, result in results.items()
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
with open(report_path, "w", encoding="utf-8") as f:
|
|
930
|
+
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
|
931
|
+
|
|
932
|
+
def _seed_context_user_input(self, context: WorkflowContext) -> None:
|
|
933
|
+
cfg = self.config_manager.config
|
|
934
|
+
defaults = {
|
|
935
|
+
"name": cfg.name or self.project_dir.name,
|
|
936
|
+
"description": cfg.description,
|
|
937
|
+
"platform": cfg.platform,
|
|
938
|
+
"frontend": cfg.frontend,
|
|
939
|
+
"backend": cfg.backend,
|
|
940
|
+
"domain": cfg.domain,
|
|
941
|
+
"cicd": "github",
|
|
942
|
+
"offline": False,
|
|
943
|
+
}
|
|
944
|
+
for key, value in defaults.items():
|
|
945
|
+
context.user_input.setdefault(key, value)
|
|
946
|
+
|
|
947
|
+
def _write_generated_files(self, generated_files: Any) -> list[str]:
|
|
948
|
+
if not isinstance(generated_files, dict):
|
|
949
|
+
return []
|
|
950
|
+
saved_paths: list[str] = []
|
|
951
|
+
for relative_path, content in generated_files.items():
|
|
952
|
+
if not isinstance(relative_path, str) or not isinstance(content, str):
|
|
953
|
+
continue
|
|
954
|
+
full_path = self.project_dir / relative_path
|
|
955
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
956
|
+
full_path.write_text(content, encoding="utf-8")
|
|
957
|
+
saved_paths.append(str(full_path))
|
|
958
|
+
return saved_paths
|