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
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()
|