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.
Files changed (61) hide show
  1. super_dev/__init__.py +11 -0
  2. super_dev/analyzer/__init__.py +34 -0
  3. super_dev/analyzer/analyzer.py +440 -0
  4. super_dev/analyzer/detectors.py +511 -0
  5. super_dev/analyzer/models.py +285 -0
  6. super_dev/cli.py +3257 -0
  7. super_dev/config/__init__.py +11 -0
  8. super_dev/config/frontend.py +557 -0
  9. super_dev/config/manager.py +281 -0
  10. super_dev/creators/__init__.py +26 -0
  11. super_dev/creators/creator.py +134 -0
  12. super_dev/creators/document_generator.py +2473 -0
  13. super_dev/creators/frontend_builder.py +371 -0
  14. super_dev/creators/implementation_builder.py +789 -0
  15. super_dev/creators/prompt_generator.py +289 -0
  16. super_dev/creators/requirement_parser.py +354 -0
  17. super_dev/creators/spec_builder.py +195 -0
  18. super_dev/deployers/__init__.py +20 -0
  19. super_dev/deployers/cicd.py +1269 -0
  20. super_dev/deployers/delivery.py +229 -0
  21. super_dev/deployers/migration.py +1032 -0
  22. super_dev/design/__init__.py +74 -0
  23. super_dev/design/aesthetics.py +530 -0
  24. super_dev/design/charts.py +396 -0
  25. super_dev/design/codegen.py +379 -0
  26. super_dev/design/engine.py +528 -0
  27. super_dev/design/generator.py +395 -0
  28. super_dev/design/landing.py +422 -0
  29. super_dev/design/tech_stack.py +524 -0
  30. super_dev/design/tokens.py +269 -0
  31. super_dev/design/ux_guide.py +391 -0
  32. super_dev/exceptions.py +119 -0
  33. super_dev/experts/__init__.py +19 -0
  34. super_dev/experts/service.py +161 -0
  35. super_dev/integrations/__init__.py +7 -0
  36. super_dev/integrations/manager.py +264 -0
  37. super_dev/orchestrator/__init__.py +12 -0
  38. super_dev/orchestrator/engine.py +958 -0
  39. super_dev/orchestrator/experts.py +423 -0
  40. super_dev/orchestrator/knowledge.py +352 -0
  41. super_dev/orchestrator/quality.py +356 -0
  42. super_dev/reviewers/__init__.py +17 -0
  43. super_dev/reviewers/code_review.py +471 -0
  44. super_dev/reviewers/quality_gate.py +964 -0
  45. super_dev/reviewers/redteam.py +881 -0
  46. super_dev/skills/__init__.py +7 -0
  47. super_dev/skills/manager.py +307 -0
  48. super_dev/specs/__init__.py +44 -0
  49. super_dev/specs/generator.py +264 -0
  50. super_dev/specs/manager.py +428 -0
  51. super_dev/specs/models.py +348 -0
  52. super_dev/specs/validator.py +415 -0
  53. super_dev/utils/__init__.py +11 -0
  54. super_dev/utils/logger.py +133 -0
  55. super_dev/web/api.py +1402 -0
  56. super_dev-2.0.0.dist-info/METADATA +252 -0
  57. super_dev-2.0.0.dist-info/RECORD +61 -0
  58. super_dev-2.0.0.dist-info/WHEEL +5 -0
  59. super_dev-2.0.0.dist-info/entry_points.txt +2 -0
  60. super_dev-2.0.0.dist-info/licenses/LICENSE +21 -0
  61. 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