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
super_dev/web/api.py ADDED
@@ -0,0 +1,1402 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 开发:Excellent(11964948@qq.com)
4
+ 功能:Super Dev Web API - FastAPI 后端
5
+ 作用:提供 REST API 服务,支持项目管理和工作流执行
6
+ 创建时间:2025-12-30
7
+ 最后修改:2025-12-30
8
+ """
9
+
10
+ import asyncio
11
+ import json
12
+ import os
13
+ import re
14
+ import zipfile
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from threading import Lock
18
+ from typing import Any, Literal, cast
19
+
20
+ import uvicorn
21
+ from fastapi import BackgroundTasks, FastAPI, HTTPException
22
+ from fastapi.middleware.cors import CORSMiddleware
23
+ from fastapi.responses import FileResponse
24
+ from fastapi.staticfiles import StaticFiles
25
+ from pydantic import BaseModel
26
+
27
+ from super_dev.config import ConfigManager
28
+ from super_dev.deployers import CICDGenerator
29
+ from super_dev.experts import (
30
+ has_expert,
31
+ list_expert_advice_history,
32
+ read_expert_advice,
33
+ save_expert_advice,
34
+ )
35
+ from super_dev.experts import (
36
+ list_experts as list_expert_catalog,
37
+ )
38
+ from super_dev.orchestrator import Phase, WorkflowContext, WorkflowEngine
39
+
40
+ # ==================== 数据模型 ====================
41
+
42
+ class ProjectInitRequest(BaseModel):
43
+ """项目初始化请求"""
44
+ name: str
45
+ description: str = ""
46
+ platform: str = "web"
47
+ frontend: str = "react"
48
+ backend: str = "node"
49
+ domain: str = ""
50
+ quality_gate: int = 80
51
+
52
+
53
+ class ProjectConfigResponse(BaseModel):
54
+ """项目配置响应"""
55
+ name: str
56
+ description: str
57
+ version: str
58
+ platform: str
59
+ frontend: str
60
+ backend: str
61
+ domain: str
62
+ quality_gate: int
63
+ phases: list[str]
64
+ experts: list[str]
65
+
66
+
67
+ class WorkflowRunRequest(BaseModel):
68
+ """工作流运行请求"""
69
+ phases: list[str] | None = None
70
+ quality_gate: int | None = None
71
+ name: str | None = None
72
+ description: str | None = None
73
+ platform: str | None = None
74
+ frontend: str | None = None
75
+ backend: str | None = None
76
+ domain: str | None = None
77
+ cicd: str | None = None
78
+ orm: str | None = None
79
+ offline: bool = False
80
+
81
+
82
+ class WorkflowRunResponse(BaseModel):
83
+ """工作流运行响应"""
84
+ status: str
85
+ message: str
86
+ run_id: str | None = None
87
+
88
+
89
+ class ExpertAdviceRequest(BaseModel):
90
+ """专家建议请求"""
91
+ prompt: str = ""
92
+
93
+
94
+ class DeployGenerateRequest(BaseModel):
95
+ """部署配置生成请求"""
96
+ cicd_platform: str = "all"
97
+ include_runtime: bool = True
98
+ overwrite: bool = True
99
+ name: str | None = None
100
+ platform: str | None = None
101
+ frontend: str | None = None
102
+ backend: str | None = None
103
+ domain: str | None = None
104
+
105
+
106
+ class DeployRemediationExportRequest(BaseModel):
107
+ """部署修复建议导出请求"""
108
+ cicd_platform: str = "all"
109
+ only_missing: bool = True
110
+ split_by_platform: bool = True
111
+ env_file_name: str = ".env.deploy.example"
112
+ checklist_file_name: str | None = None
113
+
114
+
115
+ # ==================== FastAPI 应用 ====================
116
+
117
+ app = FastAPI(
118
+ title="Super Dev API",
119
+ description="顶级 AI 开发战队 - Web API",
120
+ version="2.0.0"
121
+ )
122
+
123
+ # CORS 配置
124
+ app.add_middleware(
125
+ CORSMiddleware,
126
+ allow_origins=["*"], # 生产环境应限制域名
127
+ allow_credentials=True,
128
+ allow_methods=["*"],
129
+ allow_headers=["*"],
130
+ )
131
+
132
+ # ==================== 工作流运行状态 ====================
133
+
134
+ _RUN_STORE: dict[str, dict[str, Any]] = {}
135
+ _RUN_STORE_LOCK = Lock()
136
+
137
+ CICDPlatform = Literal["github", "gitlab", "jenkins", "azure", "bitbucket", "all"]
138
+ VALID_CICD_PLATFORMS: set[str] = {"github", "gitlab", "jenkins", "azure", "bitbucket", "all"}
139
+
140
+ _DEPLOY_CICD_FILE_MAP: dict[str, list[str]] = {
141
+ "github": [".github/workflows/ci.yml", ".github/workflows/cd.yml"],
142
+ "gitlab": [".gitlab-ci.yml"],
143
+ "jenkins": ["Jenkinsfile"],
144
+ "azure": [".azure-pipelines.yml"],
145
+ "bitbucket": ["bitbucket-pipelines.yml"],
146
+ "all": [
147
+ ".github/workflows/ci.yml",
148
+ ".github/workflows/cd.yml",
149
+ ".gitlab-ci.yml",
150
+ "Jenkinsfile",
151
+ ".azure-pipelines.yml",
152
+ "bitbucket-pipelines.yml",
153
+ ],
154
+ }
155
+
156
+ _DEPLOY_RUNTIME_FILES: list[str] = [
157
+ "Dockerfile",
158
+ "docker-compose.yml",
159
+ ".dockerignore",
160
+ "k8s/deployment.yaml",
161
+ "k8s/service.yaml",
162
+ "k8s/ingress.yaml",
163
+ "k8s/configmap.yaml",
164
+ "k8s/secret.yaml",
165
+ ]
166
+
167
+ _DEPLOY_ENV_HINTS: dict[str, list[dict[str, str]]] = {
168
+ "github": [
169
+ {"name": "DOCKER_USERNAME", "description": "Docker 镜像仓库用户名"},
170
+ {"name": "DOCKER_PASSWORD", "description": "Docker 镜像仓库密码/Token"},
171
+ {"name": "KUBE_CONFIG_DEV", "description": "开发环境 Kubernetes kubeconfig"},
172
+ {"name": "KUBE_CONFIG_PROD", "description": "生产环境 Kubernetes kubeconfig"},
173
+ ],
174
+ "gitlab": [
175
+ {"name": "CI_REGISTRY_USER", "description": "GitLab Registry 用户名"},
176
+ {"name": "CI_REGISTRY_PASSWORD", "description": "GitLab Registry 密码/Token"},
177
+ {"name": "KUBE_CONTEXT_DEV", "description": "开发环境 K8s 上下文"},
178
+ {"name": "KUBE_CONTEXT_PROD", "description": "生产环境 K8s 上下文"},
179
+ ],
180
+ "azure": [
181
+ {"name": "AZURE_ACR_SERVICE_CONNECTION", "description": "Azure ACR 服务连接标识"},
182
+ {"name": "AZURE_DEV_K8S_CONNECTION", "description": "开发环境 AKS 服务连接标识"},
183
+ {"name": "AZURE_PROD_K8S_CONNECTION", "description": "生产环境 AKS 服务连接标识"},
184
+ ],
185
+ "bitbucket": [
186
+ {"name": "REGISTRY_URL", "description": "镜像仓库地址"},
187
+ {"name": "KUBE_CONFIG_DEV", "description": "开发环境 Kubernetes kubeconfig"},
188
+ {"name": "KUBE_CONFIG_PROD", "description": "生产环境 Kubernetes kubeconfig"},
189
+ ],
190
+ }
191
+
192
+ _DEPLOY_MANUAL_HINTS: dict[str, list[str]] = {
193
+ "jenkins": [
194
+ "Jenkins Credentials: docker-credentials",
195
+ "Jenkins Credentials: kubeconfig-dev",
196
+ "Jenkins Credentials: kubeconfig-prod",
197
+ ]
198
+ }
199
+
200
+ _DEPLOY_PLATFORM_GUIDANCE: dict[str, list[str]] = {
201
+ "github": [
202
+ "在 GitHub 仓库中打开 Settings > Secrets and variables > Actions。",
203
+ "将必需变量配置为 Repository secrets,并按环境拆分 dev/prod。",
204
+ "首次执行前在 Actions 中手动触发一次 workflow 验证权限。",
205
+ ],
206
+ "gitlab": [
207
+ "在 GitLab 项目中打开 Settings > CI/CD > Variables。",
208
+ "对敏感变量启用 Masked/Protected,并限制到对应分支。",
209
+ "在 Pipeline Editor 先运行 lint 以校验 YAML 语法。",
210
+ ],
211
+ "jenkins": [
212
+ "在 Jenkins 中创建 Credentials(ID 与流水线引用一致)。",
213
+ "确认 Jenkins Agent 具备 Docker 与 kubectl 访问权限。",
214
+ "先在 develop 分支做一次 dry run,再放开生产发布节点。",
215
+ ],
216
+ "azure": [
217
+ "在 Azure DevOps 中创建 Service Connection(ACR + AKS)。",
218
+ "将关键变量放入 Variable Group,并启用 secret 保护。",
219
+ "先运行 Build stage 验证镜像推送权限,再开启 Deploy stage。",
220
+ ],
221
+ "bitbucket": [
222
+ "在 Repository settings > Pipelines > Repository variables 配置变量。",
223
+ "将 kubeconfig 作为 secured variable 保存,避免明文提交。",
224
+ "先对 pull request 流水线执行一次全流程验证。",
225
+ ],
226
+ }
227
+
228
+
229
+ def _utc_now() -> str:
230
+ return datetime.now(timezone.utc).isoformat()
231
+
232
+
233
+ def _run_state_dir(project_dir: Path) -> Path:
234
+ return project_dir / ".super-dev" / "runs"
235
+
236
+
237
+ def _run_state_file(project_dir: Path, run_id: str) -> Path:
238
+ return _run_state_dir(project_dir) / f"{run_id}.json"
239
+
240
+
241
+ def _persist_run_state(project_dir: Path, run_id: str, run: dict[str, Any]) -> None:
242
+ state_dir = _run_state_dir(project_dir)
243
+ state_dir.mkdir(parents=True, exist_ok=True)
244
+ _run_state_file(project_dir, run_id).write_text(
245
+ json.dumps(run, ensure_ascii=False, indent=2),
246
+ encoding="utf-8",
247
+ )
248
+
249
+
250
+ def _load_persisted_run_state(project_dir: Path, run_id: str) -> dict[str, Any] | None:
251
+ file_path = _run_state_file(project_dir, run_id)
252
+ if not file_path.exists():
253
+ return None
254
+ try:
255
+ data = json.loads(file_path.read_text(encoding="utf-8"))
256
+ except Exception:
257
+ return None
258
+ return data if isinstance(data, dict) else None
259
+
260
+
261
+ def _store_run_state(run_id: str, persist_dir: Path | None = None, **fields: Any) -> None:
262
+ with _RUN_STORE_LOCK:
263
+ run = _RUN_STORE.setdefault(run_id, {})
264
+ run.update(fields)
265
+ run["run_id"] = run_id
266
+ run["updated_at"] = _utc_now()
267
+ if persist_dir is not None:
268
+ _persist_run_state(persist_dir, run_id, run)
269
+
270
+
271
+ def _get_run_state(run_id: str, project_dir: Path | None = None) -> dict[str, Any] | None:
272
+ with _RUN_STORE_LOCK:
273
+ run = _RUN_STORE.get(run_id)
274
+ if run is not None:
275
+ return dict(run)
276
+
277
+ if project_dir is None:
278
+ return None
279
+
280
+ persisted = _load_persisted_run_state(project_dir, run_id)
281
+ if persisted is None:
282
+ return None
283
+
284
+ with _RUN_STORE_LOCK:
285
+ _RUN_STORE[run_id] = dict(persisted)
286
+
287
+ return persisted
288
+
289
+
290
+ def _list_persisted_runs(project_dir: Path, limit: int = 20) -> list[dict[str, Any]]:
291
+ state_dir = _run_state_dir(project_dir)
292
+ if not state_dir.exists():
293
+ return []
294
+
295
+ runs: list[dict[str, Any]] = []
296
+ files = sorted(state_dir.glob("*.json"), key=lambda f: f.stat().st_mtime, reverse=True)
297
+ for file_path in files[:limit]:
298
+ data: dict[str, Any] | None = None
299
+ try:
300
+ loaded = json.loads(file_path.read_text(encoding="utf-8"))
301
+ if isinstance(loaded, dict):
302
+ data = loaded
303
+ except Exception:
304
+ data = None
305
+ if data is not None:
306
+ runs.append(data)
307
+ return runs
308
+
309
+
310
+ def _is_cancel_requested(run_id: str) -> bool:
311
+ with _RUN_STORE_LOCK:
312
+ run = _RUN_STORE.get(run_id)
313
+ return bool(run and run.get("cancel_requested"))
314
+
315
+
316
+ def _sanitize_run_payload(value: Any, depth: int = 0) -> Any:
317
+ """将阶段输出裁剪为可安全持久化的 JSON 结构。"""
318
+ if depth > 4:
319
+ return "<truncated>"
320
+
321
+ if value is None or isinstance(value, bool | int | float):
322
+ return value
323
+
324
+ if isinstance(value, str):
325
+ return value[:500] if len(value) > 500 else value
326
+
327
+ if isinstance(value, Path):
328
+ return str(value)
329
+
330
+ if isinstance(value, dict):
331
+ out: dict[str, Any] = {}
332
+ for idx, (k, v) in enumerate(value.items()):
333
+ if idx >= 80:
334
+ out["__truncated__"] = True
335
+ break
336
+ out[str(k)] = _sanitize_run_payload(v, depth + 1)
337
+ return out
338
+
339
+ if isinstance(value, list | tuple | set):
340
+ out_list = []
341
+ for idx, item in enumerate(value):
342
+ if idx >= 120:
343
+ out_list.append("<truncated>")
344
+ break
345
+ out_list.append(_sanitize_run_payload(item, depth + 1))
346
+ return out_list
347
+
348
+ return str(value)
349
+
350
+
351
+ def _sanitize_project_name(name: str) -> str:
352
+ sanitized = re.sub(r"[^0-9a-zA-Z_-]+", "-", (name or "").strip()).strip("-")
353
+ return sanitized.lower() or "my-project"
354
+
355
+
356
+ def _resolve_deploy_targets(cicd_platform: str, include_runtime: bool) -> list[str]:
357
+ selected = list(_DEPLOY_CICD_FILE_MAP[cicd_platform])
358
+ if include_runtime:
359
+ selected.extend(_DEPLOY_RUNTIME_FILES)
360
+ return selected
361
+
362
+
363
+ def _to_cicd_platform(value: str) -> CICDPlatform:
364
+ if value not in VALID_CICD_PLATFORMS:
365
+ raise ValueError(value)
366
+ return cast(CICDPlatform, value)
367
+
368
+
369
+ def _resolve_deploy_env_hints(cicd_platform: str) -> list[dict[str, str]]:
370
+ if cicd_platform == "all":
371
+ merged: list[dict[str, str]] = []
372
+ seen = set()
373
+ for platform in ("github", "gitlab", "azure", "bitbucket"):
374
+ for item in _DEPLOY_ENV_HINTS.get(platform, []):
375
+ key = item["name"]
376
+ if key in seen:
377
+ continue
378
+ seen.add(key)
379
+ merged.append(item)
380
+ return merged
381
+ return list(_DEPLOY_ENV_HINTS.get(cicd_platform, []))
382
+
383
+
384
+ def _resolve_deploy_manual_hints(cicd_platform: str) -> list[str]:
385
+ if cicd_platform == "all":
386
+ merged: list[str] = []
387
+ seen = set()
388
+ for platform in ("jenkins",):
389
+ for item in _DEPLOY_MANUAL_HINTS.get(platform, []):
390
+ if item in seen:
391
+ continue
392
+ seen.add(item)
393
+ merged.append(item)
394
+ return merged
395
+ return list(_DEPLOY_MANUAL_HINTS.get(cicd_platform, []))
396
+
397
+
398
+ def _collect_workflow_artifact_files(project_dir_path: Path) -> list[Path]:
399
+ files: list[Path] = []
400
+ seen: set[Path] = set()
401
+
402
+ def _append(path: Path) -> None:
403
+ resolved = path.resolve()
404
+ if resolved in seen or not resolved.exists() or not resolved.is_file():
405
+ return
406
+ seen.add(resolved)
407
+ files.append(resolved)
408
+
409
+ output_dir = project_dir_path / "output"
410
+ if output_dir.exists():
411
+ for pattern in ("**/*.md", "**/*.json", "**/*.html", "**/*.css", "**/*.js", "**/*.yml", "**/*.yaml"):
412
+ for file_path in output_dir.glob(pattern):
413
+ _append(file_path)
414
+
415
+ for direct in (
416
+ project_dir_path / ".env.deploy.example",
417
+ project_dir_path / ".gitlab-ci.yml",
418
+ project_dir_path / ".azure-pipelines.yml",
419
+ project_dir_path / "Jenkinsfile",
420
+ project_dir_path / "bitbucket-pipelines.yml",
421
+ project_dir_path / "Dockerfile",
422
+ project_dir_path / "docker-compose.yml",
423
+ project_dir_path / ".dockerignore",
424
+ ):
425
+ _append(direct)
426
+
427
+ github_dir = project_dir_path / ".github" / "workflows"
428
+ if github_dir.exists():
429
+ for file_path in github_dir.glob("*.yml"):
430
+ _append(file_path)
431
+
432
+ k8s_dir = project_dir_path / "k8s"
433
+ if k8s_dir.exists():
434
+ for file_path in k8s_dir.glob("*.yaml"):
435
+ _append(file_path)
436
+
437
+ return files
438
+
439
+
440
+ def _resolve_deploy_platform_guidance(cicd_platform: str) -> list[str]:
441
+ if cicd_platform == "all":
442
+ merged: list[str] = []
443
+ seen = set()
444
+ for platform in ("github", "gitlab", "jenkins", "azure", "bitbucket"):
445
+ for item in _DEPLOY_PLATFORM_GUIDANCE.get(platform, []):
446
+ if item in seen:
447
+ continue
448
+ seen.add(item)
449
+ merged.append(item)
450
+ return merged
451
+ return list(_DEPLOY_PLATFORM_GUIDANCE.get(cicd_platform, []))
452
+
453
+
454
+ def _collect_deploy_env_items(cicd_platform: str, only_missing: bool) -> list[dict[str, Any]]:
455
+ items = []
456
+ for hint in _resolve_deploy_env_hints(cicd_platform):
457
+ name = hint["name"]
458
+ present = bool(os.getenv(name, "").strip())
459
+ if only_missing and present:
460
+ continue
461
+ items.append(
462
+ {
463
+ "name": name,
464
+ "description": hint["description"],
465
+ "present": present,
466
+ "local_export_template": f'export {name}="<value>"',
467
+ }
468
+ )
469
+ return items
470
+
471
+
472
+ def _render_deploy_env_example(cicd_platform: str, items: list[dict[str, Any]], only_missing: bool) -> str:
473
+ lines = [
474
+ "# Super Dev Deployment Environment Template",
475
+ f"# Platform: {cicd_platform}",
476
+ f"# only_missing: {str(only_missing).lower()}",
477
+ "",
478
+ ]
479
+ if not items:
480
+ lines.append("# No variables to export for current filter.")
481
+ return "\n".join(lines) + "\n"
482
+
483
+ for item in items:
484
+ lines.append(f"# {item['description']}")
485
+ lines.append(f"{item['name']}=\"<value>\"")
486
+ lines.append("")
487
+ return "\n".join(lines).rstrip() + "\n"
488
+
489
+
490
+ def _render_deploy_checklist_markdown(
491
+ cicd_platform: str,
492
+ only_missing: bool,
493
+ items: list[dict[str, Any]],
494
+ manual_requirements: list[str],
495
+ platform_guidance: list[str],
496
+ ) -> str:
497
+ lines = [
498
+ "# Deploy Remediation Checklist",
499
+ "",
500
+ f"- Platform: `{cicd_platform}`",
501
+ f"- only_missing: `{str(only_missing).lower()}`",
502
+ f"- Generated at: `{_utc_now()}`",
503
+ "",
504
+ "## Environment Variables",
505
+ "",
506
+ "| Name | Status | Description | Local Template |",
507
+ "|:---|:---:|:---|:---|",
508
+ ]
509
+
510
+ if items:
511
+ for item in items:
512
+ status = "present" if item["present"] else "missing"
513
+ lines.append(
514
+ f"| `{item['name']}` | `{status}` | {item['description']} | `{item['local_export_template']}` |"
515
+ )
516
+ else:
517
+ lines.append("| - | - | No variables in current filter | - |")
518
+
519
+ lines.extend(["", "## Platform Guidance", ""])
520
+ if platform_guidance:
521
+ for tip in platform_guidance:
522
+ lines.append(f"- {tip}")
523
+ else:
524
+ lines.append("- No guidance available.")
525
+
526
+ lines.extend(["", "## Manual Requirements", ""])
527
+ if manual_requirements:
528
+ for requirement in manual_requirements:
529
+ lines.append(f"- {requirement}")
530
+ else:
531
+ lines.append("- No manual requirements.")
532
+
533
+ return "\n".join(lines) + "\n"
534
+
535
+
536
+ def _validate_export_file_name(raw_name: str, field_name: str, default_name: str) -> str:
537
+ file_name = (raw_name or default_name).strip()
538
+ if not file_name:
539
+ file_name = default_name
540
+ if "/" in file_name or "\\" in file_name:
541
+ raise ValueError(f"{field_name} 不能包含路径分隔符")
542
+ return file_name
543
+
544
+
545
+ def _generate_deploy_remediation_files(
546
+ project_dir_path: Path,
547
+ cicd_platform: str,
548
+ only_missing: bool,
549
+ split_by_platform: bool,
550
+ env_file_name: str,
551
+ checklist_file_name: str,
552
+ ) -> dict[str, Any]:
553
+ output_dir = project_dir_path / "output" / "deploy"
554
+ output_dir.mkdir(parents=True, exist_ok=True)
555
+
556
+ remediation_items = _collect_deploy_env_items(
557
+ cicd_platform=cicd_platform,
558
+ only_missing=only_missing,
559
+ )
560
+ manual_requirements = _resolve_deploy_manual_hints(cicd_platform)
561
+ platform_guidance = _resolve_deploy_platform_guidance(cicd_platform)
562
+
563
+ env_content = _render_deploy_env_example(
564
+ cicd_platform=cicd_platform,
565
+ items=remediation_items,
566
+ only_missing=only_missing,
567
+ )
568
+ checklist_content = _render_deploy_checklist_markdown(
569
+ cicd_platform=cicd_platform,
570
+ only_missing=only_missing,
571
+ items=remediation_items,
572
+ manual_requirements=manual_requirements,
573
+ platform_guidance=platform_guidance,
574
+ )
575
+
576
+ env_path = project_dir_path / env_file_name
577
+ checklist_path = output_dir / checklist_file_name
578
+ env_path.write_text(env_content, encoding="utf-8")
579
+ checklist_path.write_text(checklist_content, encoding="utf-8")
580
+
581
+ generated_files = [str(env_path), str(checklist_path)]
582
+ per_platform_files: list[dict[str, Any]] = []
583
+ should_split = cicd_platform == "all" and split_by_platform
584
+ if should_split:
585
+ platform_dir = output_dir / "platforms"
586
+ platform_dir.mkdir(parents=True, exist_ok=True)
587
+ for platform in ("github", "gitlab", "jenkins", "azure", "bitbucket"):
588
+ platform_items = _collect_deploy_env_items(
589
+ cicd_platform=platform,
590
+ only_missing=only_missing,
591
+ )
592
+ platform_env = platform_dir / f".env.deploy.{platform}.example"
593
+ platform_checklist = platform_dir / f"{platform}-secrets-checklist.md"
594
+
595
+ platform_env.write_text(
596
+ _render_deploy_env_example(
597
+ cicd_platform=platform,
598
+ items=platform_items,
599
+ only_missing=only_missing,
600
+ ),
601
+ encoding="utf-8",
602
+ )
603
+ platform_checklist.write_text(
604
+ _render_deploy_checklist_markdown(
605
+ cicd_platform=platform,
606
+ only_missing=only_missing,
607
+ items=platform_items,
608
+ manual_requirements=_resolve_deploy_manual_hints(platform),
609
+ platform_guidance=_resolve_deploy_platform_guidance(platform),
610
+ ),
611
+ encoding="utf-8",
612
+ )
613
+
614
+ generated_files.extend([str(platform_env), str(platform_checklist)])
615
+ per_platform_files.append(
616
+ {
617
+ "platform": platform,
618
+ "items_count": len(platform_items),
619
+ "env_file": {
620
+ "file_name": platform_env.name,
621
+ "path": str(platform_env),
622
+ },
623
+ "checklist_file": {
624
+ "file_name": platform_checklist.name,
625
+ "path": str(platform_checklist),
626
+ },
627
+ }
628
+ )
629
+
630
+ return {
631
+ "remediation_items": remediation_items,
632
+ "manual_requirements": manual_requirements,
633
+ "platform_guidance": platform_guidance,
634
+ "env_path": env_path,
635
+ "checklist_path": checklist_path,
636
+ "per_platform_files": per_platform_files,
637
+ "generated_files": generated_files,
638
+ }
639
+
640
+
641
+ # ==================== API 路由 ====================
642
+
643
+ @app.get("/api/health")
644
+ async def health_check():
645
+ """健康检查"""
646
+ return {"status": "healthy", "version": "2.0.0"}
647
+
648
+
649
+ @app.get("/api/config")
650
+ async def get_config(project_dir: str = ".") -> dict:
651
+ """获取项目配置"""
652
+ try:
653
+ manager = ConfigManager(Path(project_dir))
654
+ if not manager.exists():
655
+ raise HTTPException(status_code=404, detail="项目未初始化")
656
+
657
+ config = manager.config
658
+ return {
659
+ "name": config.name,
660
+ "description": config.description,
661
+ "version": config.version,
662
+ "platform": config.platform,
663
+ "frontend": config.frontend,
664
+ "backend": config.backend,
665
+ "database": config.database,
666
+ "domain": config.domain,
667
+ "quality_gate": config.quality_gate,
668
+ "phases": config.phases,
669
+ "experts": config.experts,
670
+ "output_dir": config.output_dir,
671
+ }
672
+ except Exception as e:
673
+ raise HTTPException(status_code=500, detail=str(e))
674
+
675
+
676
+ @app.post("/api/init")
677
+ async def init_project(request: ProjectInitRequest, project_dir: str = ".") -> dict:
678
+ """初始化项目"""
679
+ try:
680
+ manager = ConfigManager(Path(project_dir))
681
+ if manager.exists():
682
+ raise HTTPException(status_code=400, detail="项目已初始化")
683
+
684
+ config = manager.create(
685
+ name=request.name,
686
+ description=request.description,
687
+ platform=request.platform,
688
+ frontend=request.frontend,
689
+ backend=request.backend,
690
+ domain=request.domain,
691
+ quality_gate=request.quality_gate
692
+ )
693
+
694
+ return {
695
+ "status": "success",
696
+ "message": "项目已初始化",
697
+ "config": {
698
+ "name": config.name,
699
+ "platform": config.platform,
700
+ "frontend": config.frontend,
701
+ "backend": config.backend,
702
+ }
703
+ }
704
+ except HTTPException:
705
+ raise
706
+ except Exception as e:
707
+ raise HTTPException(status_code=500, detail=str(e))
708
+
709
+
710
+ @app.put("/api/config")
711
+ async def update_config(
712
+ updates: dict,
713
+ project_dir: str = "."
714
+ ) -> dict:
715
+ """更新项目配置"""
716
+ try:
717
+ manager = ConfigManager(Path(project_dir))
718
+ if not manager.exists():
719
+ raise HTTPException(status_code=404, detail="项目未初始化")
720
+
721
+ config = manager.update(**updates)
722
+ return {"status": "success", "config": config.__dict__}
723
+ except HTTPException:
724
+ raise
725
+ except Exception as e:
726
+ raise HTTPException(status_code=500, detail=str(e))
727
+
728
+
729
+ @app.post("/api/workflow/run")
730
+ async def run_workflow(
731
+ request: WorkflowRunRequest,
732
+ background_tasks: BackgroundTasks,
733
+ project_dir: str = "."
734
+ ) -> WorkflowRunResponse:
735
+ """运行工作流"""
736
+ try:
737
+ project_dir_path = Path(project_dir).resolve()
738
+ manager = ConfigManager(project_dir_path)
739
+ if not manager.exists():
740
+ raise HTTPException(status_code=404, detail="项目未初始化")
741
+
742
+ # 更新质量门禁
743
+ if request.quality_gate is not None:
744
+ manager.update(quality_gate=request.quality_gate)
745
+ config = manager.config
746
+
747
+ # 解析阶段
748
+ phases = None
749
+ requested_phase_names = None
750
+ if request.phases:
751
+ phase_map = {
752
+ "discovery": Phase.DISCOVERY,
753
+ "intelligence": Phase.INTELLIGENCE,
754
+ "drafting": Phase.DRAFTING,
755
+ "redteam": Phase.REDTEAM,
756
+ "qa": Phase.QA,
757
+ "delivery": Phase.DELIVERY,
758
+ "deployment": Phase.DEPLOYMENT,
759
+ }
760
+ invalid_phases = [p for p in request.phases if p not in phase_map]
761
+ if invalid_phases:
762
+ raise HTTPException(
763
+ status_code=400,
764
+ detail=f"无效阶段: {', '.join(invalid_phases)}"
765
+ )
766
+ phases = [phase_map[p] for p in request.phases]
767
+ requested_phase_names = list(request.phases)
768
+ else:
769
+ requested_phase_names = list(manager.config.phases)
770
+
771
+ # 生成运行 ID
772
+ import uuid
773
+ run_id = str(uuid.uuid4())[:8]
774
+
775
+ _store_run_state(
776
+ run_id,
777
+ persist_dir=project_dir_path,
778
+ status="queued",
779
+ project_dir=str(project_dir_path),
780
+ requested_phases=requested_phase_names,
781
+ completed_phases=[],
782
+ progress=0,
783
+ message="工作流排队中",
784
+ cancel_requested=False,
785
+ error=None,
786
+ started_at=_utc_now(),
787
+ finished_at=None,
788
+ results=[],
789
+ )
790
+
791
+ # 后台运行工作流
792
+ async def _run_workflow_background() -> None:
793
+ if _is_cancel_requested(run_id):
794
+ _store_run_state(
795
+ run_id,
796
+ persist_dir=project_dir_path,
797
+ status="cancelled",
798
+ message="工作流已取消(启动前)",
799
+ finished_at=_utc_now(),
800
+ )
801
+ return
802
+
803
+ _store_run_state(
804
+ run_id,
805
+ persist_dir=project_dir_path,
806
+ status="running",
807
+ message="工作流运行中",
808
+ progress=5,
809
+ )
810
+ try:
811
+ engine = WorkflowEngine(project_dir_path)
812
+ context = WorkflowContext(
813
+ project_dir=project_dir_path,
814
+ config=manager,
815
+ user_input={
816
+ "name": request.name or config.name or project_dir_path.name,
817
+ "description": request.description if request.description is not None else config.description,
818
+ "platform": request.platform or config.platform,
819
+ "frontend": request.frontend or config.frontend,
820
+ "backend": request.backend or config.backend,
821
+ "domain": request.domain if request.domain is not None else config.domain,
822
+ "cicd": request.cicd or "github",
823
+ "orm": request.orm,
824
+ "offline": request.offline,
825
+ "quality_threshold": request.quality_gate,
826
+ },
827
+ )
828
+ results = await engine.run(
829
+ phases=phases,
830
+ context=context,
831
+ stop_requested=lambda: _is_cancel_requested(run_id),
832
+ )
833
+
834
+ planned = max(len(requested_phase_names or []), 1)
835
+ completed = []
836
+ serialized_results = []
837
+ for phase, result in results.items():
838
+ completed.append(phase.value)
839
+ serialized_results.append(
840
+ {
841
+ "phase": phase.value,
842
+ "success": result.success,
843
+ "duration": result.duration,
844
+ "quality_score": result.quality_score,
845
+ "errors": list(result.errors or []),
846
+ "output": _sanitize_run_payload(result.output),
847
+ }
848
+ )
849
+
850
+ all_success = all(item["success"] for item in serialized_results) if serialized_results else True
851
+ progress = min(100, int(len(completed) / planned * 100))
852
+ if _is_cancel_requested(run_id):
853
+ status = "cancelled"
854
+ message = "工作流已取消"
855
+ else:
856
+ status = "completed" if all_success and progress >= 100 else "failed"
857
+ message = "工作流执行完成" if status == "completed" else "工作流执行失败"
858
+
859
+ _store_run_state(
860
+ run_id,
861
+ persist_dir=project_dir_path,
862
+ status=status,
863
+ message=message,
864
+ completed_phases=completed,
865
+ progress=progress,
866
+ results=serialized_results,
867
+ finished_at=_utc_now(),
868
+ )
869
+ except Exception as e:
870
+ _store_run_state(
871
+ run_id,
872
+ persist_dir=project_dir_path,
873
+ status="failed",
874
+ message="工作流执行异常",
875
+ error=str(e),
876
+ finished_at=_utc_now(),
877
+ )
878
+
879
+ def _run_workflow_background_sync() -> None:
880
+ asyncio.run(_run_workflow_background())
881
+
882
+ background_tasks.add_task(_run_workflow_background_sync)
883
+
884
+ return WorkflowRunResponse(
885
+ status="started",
886
+ message=f"工作流已启动 (ID: {run_id})",
887
+ run_id=run_id
888
+ )
889
+
890
+ except HTTPException:
891
+ raise
892
+ except Exception as e:
893
+ raise HTTPException(status_code=500, detail=str(e))
894
+
895
+
896
+ @app.get("/api/workflow/status/{run_id}")
897
+ async def get_workflow_status(run_id: str, project_dir: str = ".") -> dict:
898
+ """获取工作流状态"""
899
+ run = _get_run_state(run_id, Path(project_dir).resolve())
900
+ if run is None:
901
+ raise HTTPException(status_code=404, detail=f"运行记录不存在: {run_id}")
902
+ return run
903
+
904
+
905
+ @app.post("/api/workflow/cancel/{run_id}")
906
+ async def cancel_workflow(run_id: str, project_dir: str = ".") -> dict:
907
+ """取消工作流运行"""
908
+ project_dir_path = Path(project_dir).resolve()
909
+ run = _get_run_state(run_id, project_dir_path)
910
+ if run is None:
911
+ raise HTTPException(status_code=404, detail=f"运行记录不存在: {run_id}")
912
+
913
+ status = str(run.get("status", "unknown"))
914
+ if status in {"completed", "failed", "cancelled"}:
915
+ return {
916
+ "run_id": run_id,
917
+ "status": status,
918
+ "message": "运行已结束,无需取消",
919
+ }
920
+
921
+ if status == "queued":
922
+ _store_run_state(
923
+ run_id,
924
+ persist_dir=project_dir_path,
925
+ status="cancelled",
926
+ cancel_requested=True,
927
+ message="工作流已取消(未开始执行)",
928
+ finished_at=_utc_now(),
929
+ )
930
+ return {
931
+ "run_id": run_id,
932
+ "status": "cancelled",
933
+ "message": "工作流已取消",
934
+ }
935
+
936
+ _store_run_state(
937
+ run_id,
938
+ persist_dir=project_dir_path,
939
+ status="cancelling",
940
+ cancel_requested=True,
941
+ message="已收到取消请求,将在当前阶段完成后停止",
942
+ )
943
+ return {
944
+ "run_id": run_id,
945
+ "status": "cancelling",
946
+ "message": "取消请求已受理",
947
+ }
948
+
949
+
950
+ @app.get("/api/workflow/runs")
951
+ async def list_workflow_runs(project_dir: str = ".", limit: int = 20) -> dict:
952
+ """列出工作流运行历史(最近优先)"""
953
+ if limit < 1:
954
+ raise HTTPException(status_code=400, detail="limit 必须大于 0")
955
+
956
+ project_dir_path = Path(project_dir).resolve()
957
+ runs = _list_persisted_runs(project_dir_path, limit=limit)
958
+ return {"runs": runs, "count": len(runs)}
959
+
960
+
961
+ @app.get("/api/workflow/artifacts/{run_id}")
962
+ async def list_workflow_artifacts(run_id: str, project_dir: str = ".") -> dict:
963
+ """列出某次工作流可下载的交付物文件。"""
964
+ requested_project_dir = Path(project_dir).resolve()
965
+ run = _get_run_state(run_id, requested_project_dir)
966
+ if run is None:
967
+ raise HTTPException(status_code=404, detail=f"运行记录不存在: {run_id}")
968
+
969
+ run_project_dir = Path(run.get("project_dir") or requested_project_dir).resolve()
970
+ artifact_files = _collect_workflow_artifact_files(run_project_dir)
971
+ items = []
972
+ for file_path in artifact_files:
973
+ try:
974
+ relative = str(file_path.relative_to(run_project_dir))
975
+ except ValueError:
976
+ relative = file_path.name
977
+ items.append(
978
+ {
979
+ "name": file_path.name,
980
+ "path": str(file_path),
981
+ "relative_path": str(relative),
982
+ "size_bytes": file_path.stat().st_size,
983
+ }
984
+ )
985
+
986
+ return {
987
+ "run_id": run_id,
988
+ "project_dir": str(run_project_dir),
989
+ "count": len(items),
990
+ "items": items,
991
+ }
992
+
993
+
994
+ @app.get("/api/workflow/artifacts/{run_id}/archive")
995
+ async def download_workflow_artifacts_archive(run_id: str, project_dir: str = ".") -> FileResponse:
996
+ """下载某次工作流交付物压缩包。"""
997
+ requested_project_dir = Path(project_dir).resolve()
998
+ run = _get_run_state(run_id, requested_project_dir)
999
+ if run is None:
1000
+ raise HTTPException(status_code=404, detail=f"运行记录不存在: {run_id}")
1001
+
1002
+ run_project_dir = Path(run.get("project_dir") or requested_project_dir).resolve()
1003
+ artifact_files = _collect_workflow_artifact_files(run_project_dir)
1004
+ if not artifact_files:
1005
+ raise HTTPException(status_code=404, detail="未找到可下载的交付物文件")
1006
+
1007
+ archive_dir = run_project_dir / "output" / "artifacts"
1008
+ archive_dir.mkdir(parents=True, exist_ok=True)
1009
+ archive_path = archive_dir / f"workflow-{run_id}-artifacts.zip"
1010
+
1011
+ with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
1012
+ for file_path in artifact_files:
1013
+ try:
1014
+ arcname = str(file_path.relative_to(run_project_dir))
1015
+ except ValueError:
1016
+ arcname = file_path.name
1017
+ zf.write(file_path, arcname=arcname)
1018
+
1019
+ return FileResponse(
1020
+ path=archive_path,
1021
+ media_type="application/zip",
1022
+ filename=archive_path.name,
1023
+ )
1024
+
1025
+
1026
+ @app.get("/api/experts")
1027
+ async def list_experts() -> dict:
1028
+ """列出可用专家"""
1029
+ return {"experts": list_expert_catalog()}
1030
+
1031
+
1032
+ @app.post("/api/experts/{expert_id}/advice")
1033
+ async def generate_expert_advice(
1034
+ expert_id: str,
1035
+ request: ExpertAdviceRequest,
1036
+ project_dir: str = ".",
1037
+ ) -> dict:
1038
+ """生成专家建议并写入 output 目录。"""
1039
+ expert_id = expert_id.upper()
1040
+ if not has_expert(expert_id):
1041
+ raise HTTPException(status_code=404, detail=f"未知专家: {expert_id}")
1042
+
1043
+ project_dir_path = Path(project_dir).resolve()
1044
+ file_path, content = save_expert_advice(
1045
+ project_dir=project_dir_path,
1046
+ expert_id=expert_id,
1047
+ prompt=request.prompt,
1048
+ )
1049
+ return {
1050
+ "expert_id": expert_id,
1051
+ "project_dir": str(project_dir_path),
1052
+ "file_path": str(file_path),
1053
+ "file_name": file_path.name,
1054
+ "content": content,
1055
+ }
1056
+
1057
+
1058
+ @app.get("/api/experts/advice/history")
1059
+ async def get_expert_advice_history(project_dir: str = ".", limit: int = 20) -> dict:
1060
+ """列出已生成的专家建议历史。"""
1061
+ if limit < 1:
1062
+ raise HTTPException(status_code=400, detail="limit 必须大于 0")
1063
+
1064
+ project_dir_path = Path(project_dir).resolve()
1065
+ items = list_expert_advice_history(project_dir_path, limit=limit)
1066
+ return {"items": items, "count": len(items)}
1067
+
1068
+
1069
+ @app.get("/api/experts/advice/content")
1070
+ async def get_expert_advice_content(file_name: str, project_dir: str = ".") -> dict:
1071
+ """读取某个专家建议内容。"""
1072
+ project_dir_path = Path(project_dir).resolve()
1073
+ try:
1074
+ file_path, content = read_expert_advice(project_dir_path, file_name)
1075
+ except FileNotFoundError:
1076
+ raise HTTPException(status_code=404, detail=f"建议文件不存在: {file_name}")
1077
+ except ValueError as e:
1078
+ raise HTTPException(status_code=400, detail=str(e))
1079
+
1080
+ return {
1081
+ "file_name": file_path.name,
1082
+ "file_path": str(file_path),
1083
+ "content": content,
1084
+ }
1085
+
1086
+
1087
+ @app.get("/api/phases")
1088
+ async def list_phases() -> dict:
1089
+ """列出工作流阶段"""
1090
+ phases = [
1091
+ {"id": "discovery", "name": "需求发现", "description": "收集和分析用户需求"},
1092
+ {"id": "intelligence", "name": "情报收集", "description": "市场研究、竞品分析"},
1093
+ {"id": "drafting", "name": "专家起草", "description": "专家协作生成文档"},
1094
+ {"id": "redteam", "name": "红队审查", "description": "安全、性能审查"},
1095
+ {"id": "qa", "name": "质量门禁", "description": "质量检查和验证"},
1096
+ {"id": "delivery", "name": "幻影交付", "description": "生成原型预览"},
1097
+ {"id": "deployment", "name": "工业化部署", "description": "生成部署配置"},
1098
+ ]
1099
+ return {"phases": phases}
1100
+
1101
+
1102
+ @app.get("/api/deploy/platforms")
1103
+ async def list_deploy_platforms() -> dict:
1104
+ """列出支持的 CI/CD 平台"""
1105
+ return {
1106
+ "platforms": [
1107
+ {"id": "all", "name": "全部平台"},
1108
+ {"id": "github", "name": "GitHub Actions"},
1109
+ {"id": "gitlab", "name": "GitLab CI"},
1110
+ {"id": "jenkins", "name": "Jenkins"},
1111
+ {"id": "azure", "name": "Azure DevOps"},
1112
+ {"id": "bitbucket", "name": "Bitbucket Pipelines"},
1113
+ ]
1114
+ }
1115
+
1116
+
1117
+ @app.get("/api/deploy/precheck")
1118
+ async def precheck_deploy_configs(
1119
+ cicd_platform: str = "all",
1120
+ include_runtime: bool = True,
1121
+ project_dir: str = ".",
1122
+ ) -> dict:
1123
+ """部署生成前预检(变量与目标文件状态)。"""
1124
+ valid_platforms = {"github", "gitlab", "jenkins", "azure", "bitbucket", "all"}
1125
+ if cicd_platform not in valid_platforms:
1126
+ raise HTTPException(
1127
+ status_code=400,
1128
+ detail=f"不支持的 cicd_platform: {cicd_platform}",
1129
+ )
1130
+
1131
+ project_dir_path = Path(project_dir).resolve()
1132
+ target_files = _resolve_deploy_targets(cicd_platform, include_runtime)
1133
+ existing_files = [p for p in target_files if (project_dir_path / p).exists()]
1134
+ missing_files = [p for p in target_files if not (project_dir_path / p).exists()]
1135
+
1136
+ env_hints = _resolve_deploy_env_hints(cicd_platform)
1137
+ env_checks = []
1138
+ for item in env_hints:
1139
+ name = item["name"]
1140
+ present = bool(os.getenv(name, "").strip())
1141
+ env_checks.append(
1142
+ {
1143
+ "name": name,
1144
+ "description": item["description"],
1145
+ "present": present,
1146
+ }
1147
+ )
1148
+
1149
+ manual_requirements = _resolve_deploy_manual_hints(cicd_platform)
1150
+ missing_env = [item["name"] for item in env_checks if not item["present"]]
1151
+
1152
+ return {
1153
+ "status": "success",
1154
+ "project_dir": str(project_dir_path),
1155
+ "cicd_platform": cicd_platform,
1156
+ "include_runtime": include_runtime,
1157
+ "target_count": len(target_files),
1158
+ "existing_count": len(existing_files),
1159
+ "missing_count": len(missing_files),
1160
+ "existing_files": existing_files,
1161
+ "missing_files": missing_files,
1162
+ "env_checks": env_checks,
1163
+ "missing_env": missing_env,
1164
+ "manual_requirements": manual_requirements,
1165
+ "ready_for_generate": len(missing_env) == 0,
1166
+ }
1167
+
1168
+
1169
+ @app.get("/api/deploy/remediation")
1170
+ async def get_deploy_remediation(
1171
+ cicd_platform: str = "all",
1172
+ only_missing: bool = True,
1173
+ ) -> dict:
1174
+ """获取部署修复建议(环境变量模板 + 平台操作指引)。"""
1175
+ valid_platforms = {"github", "gitlab", "jenkins", "azure", "bitbucket", "all"}
1176
+ if cicd_platform not in valid_platforms:
1177
+ raise HTTPException(
1178
+ status_code=400,
1179
+ detail=f"不支持的 cicd_platform: {cicd_platform}",
1180
+ )
1181
+
1182
+ remediation_items = _collect_deploy_env_items(
1183
+ cicd_platform=cicd_platform,
1184
+ only_missing=only_missing,
1185
+ )
1186
+
1187
+ return {
1188
+ "status": "success",
1189
+ "cicd_platform": cicd_platform,
1190
+ "only_missing": only_missing,
1191
+ "items": remediation_items,
1192
+ "manual_requirements": _resolve_deploy_manual_hints(cicd_platform),
1193
+ "platform_guidance": _resolve_deploy_platform_guidance(cicd_platform),
1194
+ }
1195
+
1196
+
1197
+ @app.post("/api/deploy/remediation/export")
1198
+ async def export_deploy_remediation(
1199
+ request: DeployRemediationExportRequest,
1200
+ project_dir: str = ".",
1201
+ ) -> dict:
1202
+ """导出部署修复模板文件(env 示例 + checklist)。"""
1203
+ valid_platforms = {"github", "gitlab", "jenkins", "azure", "bitbucket", "all"}
1204
+ if request.cicd_platform not in valid_platforms:
1205
+ raise HTTPException(
1206
+ status_code=400,
1207
+ detail=f"不支持的 cicd_platform: {request.cicd_platform}",
1208
+ )
1209
+
1210
+ project_dir_path = Path(project_dir).resolve()
1211
+ try:
1212
+ env_file_name = _validate_export_file_name(
1213
+ raw_name=request.env_file_name,
1214
+ field_name="env_file_name",
1215
+ default_name=".env.deploy.example",
1216
+ )
1217
+ checklist_name = _validate_export_file_name(
1218
+ raw_name=request.checklist_file_name or "",
1219
+ field_name="checklist_file_name",
1220
+ default_name=f"{request.cicd_platform}-secrets-checklist.md",
1221
+ )
1222
+ except ValueError as e:
1223
+ raise HTTPException(status_code=400, detail=str(e))
1224
+
1225
+ generated = _generate_deploy_remediation_files(
1226
+ project_dir_path=project_dir_path,
1227
+ cicd_platform=request.cicd_platform,
1228
+ only_missing=request.only_missing,
1229
+ split_by_platform=request.split_by_platform,
1230
+ env_file_name=env_file_name,
1231
+ checklist_file_name=checklist_name,
1232
+ )
1233
+
1234
+ env_path = generated["env_path"]
1235
+ checklist_path = generated["checklist_path"]
1236
+ remediation_items = generated["remediation_items"]
1237
+
1238
+ return {
1239
+ "status": "success",
1240
+ "project_dir": str(project_dir_path),
1241
+ "cicd_platform": request.cicd_platform,
1242
+ "only_missing": request.only_missing,
1243
+ "split_by_platform": request.split_by_platform,
1244
+ "env_file": {
1245
+ "file_name": env_path.name,
1246
+ "path": str(env_path),
1247
+ },
1248
+ "checklist_file": {
1249
+ "file_name": checklist_path.name,
1250
+ "path": str(checklist_path),
1251
+ },
1252
+ "items_count": len(remediation_items),
1253
+ "generated_files": generated["generated_files"],
1254
+ "per_platform_files": generated["per_platform_files"],
1255
+ }
1256
+
1257
+
1258
+ @app.get("/api/deploy/remediation/archive")
1259
+ async def download_deploy_remediation_archive(
1260
+ cicd_platform: str = "all",
1261
+ only_missing: bool = True,
1262
+ split_by_platform: bool = True,
1263
+ project_dir: str = ".",
1264
+ ) -> FileResponse:
1265
+ """生成并下载部署修复模板压缩包。"""
1266
+ valid_platforms = {"github", "gitlab", "jenkins", "azure", "bitbucket", "all"}
1267
+ if cicd_platform not in valid_platforms:
1268
+ raise HTTPException(
1269
+ status_code=400,
1270
+ detail=f"不支持的 cicd_platform: {cicd_platform}",
1271
+ )
1272
+
1273
+ project_dir_path = Path(project_dir).resolve()
1274
+ generated = _generate_deploy_remediation_files(
1275
+ project_dir_path=project_dir_path,
1276
+ cicd_platform=cicd_platform,
1277
+ only_missing=only_missing,
1278
+ split_by_platform=split_by_platform,
1279
+ env_file_name=".env.deploy.example",
1280
+ checklist_file_name=f"{cicd_platform}-secrets-checklist.md",
1281
+ )
1282
+
1283
+ output_dir = project_dir_path / "output" / "deploy"
1284
+ output_dir.mkdir(parents=True, exist_ok=True)
1285
+ zip_path = output_dir / f"deploy-remediation-{cicd_platform}.zip"
1286
+
1287
+ with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
1288
+ for file_path_str in generated["generated_files"]:
1289
+ file_path = Path(file_path_str)
1290
+ if not file_path.exists():
1291
+ continue
1292
+ try:
1293
+ arcname = str(file_path.relative_to(project_dir_path))
1294
+ except ValueError:
1295
+ arcname = file_path.name
1296
+ zf.write(file_path, arcname=arcname)
1297
+
1298
+ return FileResponse(
1299
+ path=zip_path,
1300
+ media_type="application/zip",
1301
+ filename=zip_path.name,
1302
+ )
1303
+
1304
+
1305
+ @app.post("/api/deploy/generate")
1306
+ async def generate_deploy_configs(
1307
+ request: DeployGenerateRequest,
1308
+ project_dir: str = ".",
1309
+ ) -> dict:
1310
+ """生成部署配置(CI/CD + 可选 Docker/K8s)。"""
1311
+ if request.cicd_platform not in VALID_CICD_PLATFORMS:
1312
+ raise HTTPException(
1313
+ status_code=400,
1314
+ detail=f"不支持的 cicd_platform: {request.cicd_platform}",
1315
+ )
1316
+ platform = _to_cicd_platform(request.cicd_platform)
1317
+
1318
+ project_dir_path = Path(project_dir).resolve()
1319
+ config_manager = ConfigManager(project_dir_path)
1320
+ config = config_manager.config
1321
+
1322
+ project_name = _sanitize_project_name(request.name or config.name or project_dir_path.name)
1323
+ tech_stack = {
1324
+ "platform": request.platform or config.platform,
1325
+ "frontend": request.frontend or config.frontend,
1326
+ "backend": request.backend or config.backend,
1327
+ "domain": request.domain or config.domain,
1328
+ }
1329
+
1330
+ generator = CICDGenerator(
1331
+ project_dir=project_dir_path,
1332
+ name=project_name,
1333
+ tech_stack=tech_stack,
1334
+ platform=platform,
1335
+ )
1336
+ generated_files = generator.generate()
1337
+
1338
+ selected_keys = _resolve_deploy_targets(
1339
+ cicd_platform=request.cicd_platform,
1340
+ include_runtime=request.include_runtime,
1341
+ )
1342
+ selected_keys = [key for key in selected_keys if key in generated_files]
1343
+
1344
+ written_files: list[str] = []
1345
+ skipped_files: list[str] = []
1346
+ for relative_path in selected_keys:
1347
+ full_path = project_dir_path / relative_path
1348
+ if full_path.exists() and not request.overwrite:
1349
+ skipped_files.append(relative_path)
1350
+ continue
1351
+ full_path.parent.mkdir(parents=True, exist_ok=True)
1352
+ full_path.write_text(generated_files[relative_path], encoding="utf-8")
1353
+ written_files.append(relative_path)
1354
+
1355
+ return {
1356
+ "status": "success",
1357
+ "project_dir": str(project_dir_path),
1358
+ "project_name": project_name,
1359
+ "cicd_platform": request.cicd_platform,
1360
+ "include_runtime": request.include_runtime,
1361
+ "overwrite": request.overwrite,
1362
+ "generated_count": len(written_files),
1363
+ "skipped_count": len(skipped_files),
1364
+ "generated_files": written_files,
1365
+ "skipped_files": skipped_files,
1366
+ }
1367
+
1368
+
1369
+ def _mount_frontend_if_present() -> None:
1370
+ """在 API 路由注册完成后挂载前端,避免遮蔽 /api 路由。"""
1371
+ frontend_path = Path(__file__).parent / "frontend" / "dist"
1372
+ if not frontend_path.exists():
1373
+ return
1374
+
1375
+ for route in app.routes:
1376
+ if getattr(route, "name", "") == "frontend":
1377
+ return
1378
+
1379
+ app.mount("/", StaticFiles(directory=str(frontend_path), html=True), name="frontend")
1380
+
1381
+
1382
+ _mount_frontend_if_present()
1383
+
1384
+
1385
+ # ==================== 主函数 ====================
1386
+
1387
+ def main():
1388
+ """启动 API 服务器"""
1389
+ host = os.getenv("SUPER_DEV_API_HOST", "127.0.0.1")
1390
+ port = int(os.getenv("SUPER_DEV_API_PORT", "8000"))
1391
+ reload_enabled = os.getenv("SUPER_DEV_API_RELOAD", "true").strip().lower() in {"1", "true", "yes", "on"}
1392
+ uvicorn.run(
1393
+ "super_dev.web.api:app",
1394
+ host=host,
1395
+ port=port,
1396
+ reload=reload_enabled,
1397
+ log_level="info"
1398
+ )
1399
+
1400
+
1401
+ if __name__ == "__main__":
1402
+ main()