full-stack-coding-assistant-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +0 -0
- agents/audit_agent.py +223 -0
- agents/backend_agent.py +179 -0
- agents/base_agent.py +406 -0
- agents/frontend_agent.py +148 -0
- agents/test_agent.py +155 -0
- coordinator/__init__.py +0 -0
- coordinator/coordinator.py +452 -0
- coordinator/dag.py +147 -0
- executor/__init__.py +0 -0
- executor/cb_integration.py +160 -0
- full_stack_coding_assistant_agent/__init__.py +6 -0
- full_stack_coding_assistant_agent/cli.py +10 -0
- full_stack_coding_assistant_agent/main.py +686 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/METADATA +849 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/RECORD +31 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/WHEEL +5 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/entry_points.txt +2 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/top_level.txt +7 -0
- model/__init__.py +0 -0
- model/config.py +62 -0
- model/model_router.py +150 -0
- storage/__init__.py +0 -0
- storage/context_db.py +274 -0
- utils/__init__.py +0 -0
- utils/agent_selector.py +243 -0
- utils/config_validator.py +143 -0
- utils/logger.py +95 -0
- utils/output_manager.py +1572 -0
- utils/pdf_reader.py +122 -0
- utils/version.py +188 -0
utils/output_manager.py
ADDED
|
@@ -0,0 +1,1572 @@
|
|
|
1
|
+
"""
|
|
2
|
+
输出目录管理器 - 负责输出目录的完整生命周期管理
|
|
3
|
+
包括:目录创建、加载已有项目、迭代记录、项目上下文收集
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OutputManager:
|
|
14
|
+
"""管理 Agent 输出目录的创建、加载和迭代记录"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, base_output_dir: str = "output"):
|
|
17
|
+
"""
|
|
18
|
+
初始化输出管理器
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
base_output_dir: 输出根目录,相对于工作目录
|
|
22
|
+
"""
|
|
23
|
+
self.base_output_dir = Path(base_output_dir)
|
|
24
|
+
self.base_output_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------ #
|
|
27
|
+
# 目录创建
|
|
28
|
+
# ------------------------------------------------------------------ #
|
|
29
|
+
|
|
30
|
+
def create_output_dir(self, task_description: str) -> Path:
|
|
31
|
+
"""
|
|
32
|
+
创建新的输出目录
|
|
33
|
+
|
|
34
|
+
目录命名格式: output/{YYYYMMDD}_{HHMMSS}_{任务摘要}/
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
task_description: 任务描述,用于生成目录名摘要
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
创建的目录 Path 对象
|
|
41
|
+
"""
|
|
42
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
43
|
+
summary = self._slugify(task_description)[:20] # 最多取前 20 个字符
|
|
44
|
+
dir_name = f"{timestamp}_{summary}"
|
|
45
|
+
output_path = self.base_output_dir / dir_name
|
|
46
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# 创建子目录
|
|
49
|
+
(output_path / "backend").mkdir(exist_ok=True)
|
|
50
|
+
(output_path / "frontend").mkdir(exist_ok=True)
|
|
51
|
+
(output_path / "tests").mkdir(exist_ok=True)
|
|
52
|
+
(output_path / "audits").mkdir(exist_ok=True)
|
|
53
|
+
(output_path / "iterations").mkdir(exist_ok=True)
|
|
54
|
+
(output_path / "traces").mkdir(exist_ok=True)
|
|
55
|
+
(output_path / "traces" / "backend").mkdir(exist_ok=True)
|
|
56
|
+
(output_path / "traces" / "frontend").mkdir(exist_ok=True)
|
|
57
|
+
(output_path / "traces" / "test").mkdir(exist_ok=True)
|
|
58
|
+
(output_path / "traces" / "audit").mkdir(exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# 创建元数据文件
|
|
61
|
+
meta = {
|
|
62
|
+
"task_description": task_description,
|
|
63
|
+
"requirements": "",
|
|
64
|
+
"created_at": datetime.now().isoformat(),
|
|
65
|
+
"updated_at": datetime.now().isoformat(),
|
|
66
|
+
"iteration_count": 0,
|
|
67
|
+
"last_task_id": "",
|
|
68
|
+
"status": "running",
|
|
69
|
+
}
|
|
70
|
+
self._write_meta(output_path, meta)
|
|
71
|
+
|
|
72
|
+
# 创建初始迭代记录
|
|
73
|
+
self.save_iteration(
|
|
74
|
+
output_path=output_path,
|
|
75
|
+
prompt=task_description,
|
|
76
|
+
result={"type": "initial", "message": "项目初始化"},
|
|
77
|
+
is_initial=True,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return output_path
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------ #
|
|
83
|
+
# 目录加载
|
|
84
|
+
# ------------------------------------------------------------------ #
|
|
85
|
+
|
|
86
|
+
def load_output_dir(self, output_path: str) -> Dict:
|
|
87
|
+
"""
|
|
88
|
+
加载已有输出目录,收集项目上下文
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
output_path: 输出目录路径(相对于工作目录或绝对路径)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
包含项目上下文的字典:
|
|
95
|
+
{
|
|
96
|
+
"output_path": Path,
|
|
97
|
+
"meta": dict,
|
|
98
|
+
"backend_code": str, # backend/ 下所有代码合并
|
|
99
|
+
"frontend_code": str, # frontend/ 下所有代码合并
|
|
100
|
+
"test_code": str, # tests/ 下所有代码合并
|
|
101
|
+
"audit_reports": list, # audits/ 下所有报告内容列表
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
path = Path(output_path)
|
|
105
|
+
if not path.is_absolute():
|
|
106
|
+
path = Path.cwd() / path
|
|
107
|
+
path = path.resolve()
|
|
108
|
+
|
|
109
|
+
if not path.exists():
|
|
110
|
+
raise FileNotFoundError(f"输出目录不存在: {path}")
|
|
111
|
+
|
|
112
|
+
meta = self._read_meta(path)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"output_path": path,
|
|
116
|
+
"meta": meta,
|
|
117
|
+
"backend_code": self._collect_code(path / "backend"),
|
|
118
|
+
"frontend_code": self._collect_code(path / "frontend"),
|
|
119
|
+
"test_code": self._collect_code(path / "tests"),
|
|
120
|
+
"audit_reports": self._collect_audits(path / "audits"),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------ #
|
|
124
|
+
# 迭代记录
|
|
125
|
+
# ------------------------------------------------------------------ #
|
|
126
|
+
|
|
127
|
+
def save_iteration(
|
|
128
|
+
self,
|
|
129
|
+
output_path: Path,
|
|
130
|
+
prompt: str,
|
|
131
|
+
result: Dict,
|
|
132
|
+
is_initial: bool = False,
|
|
133
|
+
):
|
|
134
|
+
"""
|
|
135
|
+
保存迭代记录到 iterations/ 目录
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
output_path: 输出目录 Path
|
|
139
|
+
prompt: 本次迭代的用户提示词
|
|
140
|
+
result: 执行结果字典
|
|
141
|
+
is_initial: 是否为初始创建记录
|
|
142
|
+
"""
|
|
143
|
+
meta = self._read_meta(output_path)
|
|
144
|
+
count = meta.get("iteration_count", 0)
|
|
145
|
+
if not is_initial:
|
|
146
|
+
count += 1
|
|
147
|
+
meta["iteration_count"] = count
|
|
148
|
+
meta["updated_at"] = datetime.now().isoformat()
|
|
149
|
+
|
|
150
|
+
if is_initial:
|
|
151
|
+
filename = "001_initial.md"
|
|
152
|
+
else:
|
|
153
|
+
# 用 prompt 前 30 个字符作为文件名摘要
|
|
154
|
+
prompt_summary = self._slugify(prompt)[:30]
|
|
155
|
+
filename = f"{count:03d}_{prompt_summary}.md"
|
|
156
|
+
|
|
157
|
+
iterations_dir = output_path / "iterations"
|
|
158
|
+
iterations_dir.mkdir(exist_ok=True)
|
|
159
|
+
file_path = iterations_dir / filename
|
|
160
|
+
|
|
161
|
+
content = self._format_iteration_content(prompt, result, meta)
|
|
162
|
+
file_path.write_text(content, encoding="utf-8")
|
|
163
|
+
|
|
164
|
+
self._write_meta(output_path, meta)
|
|
165
|
+
|
|
166
|
+
# ------------------------------------------------------------------ #
|
|
167
|
+
# 项目上下文收集(供 Agent prompt 注入)
|
|
168
|
+
# ------------------------------------------------------------------ #
|
|
169
|
+
|
|
170
|
+
def get_project_context(self, output_path: Path) -> str:
|
|
171
|
+
"""
|
|
172
|
+
收集项目上下文,格式化为可供 LLM prompt 注入的文本
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
output_path: 输出目录 Path
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
格式化的项目上下文字符串
|
|
179
|
+
"""
|
|
180
|
+
context = self.load_output_dir(str(output_path))
|
|
181
|
+
meta = context["meta"]
|
|
182
|
+
|
|
183
|
+
lines = [
|
|
184
|
+
"## 已有项目上下文",
|
|
185
|
+
f"- 项目描述: {meta.get('task_description', '')}",
|
|
186
|
+
f"- 创建时间: {meta.get('created_at', '')}",
|
|
187
|
+
f"- 迭代次数: {meta.get('iteration_count', 0)}",
|
|
188
|
+
"",
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
if context["backend_code"].strip():
|
|
192
|
+
lines += [
|
|
193
|
+
"### 已有后端代码",
|
|
194
|
+
"```python",
|
|
195
|
+
context["backend_code"],
|
|
196
|
+
"```",
|
|
197
|
+
"",
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
if context["frontend_code"].strip():
|
|
201
|
+
lines += [
|
|
202
|
+
"### 已有前端代码",
|
|
203
|
+
"```tsx",
|
|
204
|
+
context["frontend_code"],
|
|
205
|
+
"```",
|
|
206
|
+
"",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
if context["test_code"].strip():
|
|
210
|
+
lines += [
|
|
211
|
+
"### 已有测试代码",
|
|
212
|
+
"```python",
|
|
213
|
+
context["test_code"],
|
|
214
|
+
"```",
|
|
215
|
+
"",
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
if context["audit_reports"]:
|
|
219
|
+
lines += [
|
|
220
|
+
"### 已有审计报告摘要",
|
|
221
|
+
"",
|
|
222
|
+
]
|
|
223
|
+
for report in context["audit_reports"][-3:]: # 只取最近 3 份
|
|
224
|
+
lines.append(f"- {report[:200]}...")
|
|
225
|
+
|
|
226
|
+
return "\n".join(lines)
|
|
227
|
+
|
|
228
|
+
def get_recent_iterations(self, output_path: Path, count: int = 3) -> str:
|
|
229
|
+
"""
|
|
230
|
+
获取最近的迭代记录摘要,用于注入到 Agent prompt
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
output_path: 输出目录 Path
|
|
234
|
+
count: 获取的迭代记录数量
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
格式化的迭代历史字符串
|
|
238
|
+
"""
|
|
239
|
+
iterations_dir = output_path / "iterations"
|
|
240
|
+
if not iterations_dir.exists():
|
|
241
|
+
return "(无迭代记录)"
|
|
242
|
+
|
|
243
|
+
files = sorted(iterations_dir.glob("*.md"), reverse=True)
|
|
244
|
+
if not files:
|
|
245
|
+
return "(无迭代记录)"
|
|
246
|
+
|
|
247
|
+
lines = ["## 迭代历史", ""]
|
|
248
|
+
for f in files[:count]:
|
|
249
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
250
|
+
# 只取前 500 字符作为摘要
|
|
251
|
+
summary = content[:500].replace("\n", " ").strip()
|
|
252
|
+
lines.append(f"- {f.name}: {summary}")
|
|
253
|
+
|
|
254
|
+
return "\n".join(lines)
|
|
255
|
+
|
|
256
|
+
# ------------------------------------------------------------------ #
|
|
257
|
+
# Agent Trace 保存(可审计的完整 LLM 输入输出记录)
|
|
258
|
+
# ------------------------------------------------------------------ #
|
|
259
|
+
|
|
260
|
+
def save_trace(
|
|
261
|
+
self,
|
|
262
|
+
output_path: Path,
|
|
263
|
+
agent_type: str,
|
|
264
|
+
task_id: str,
|
|
265
|
+
system_prompt: str,
|
|
266
|
+
user_prompt: str,
|
|
267
|
+
llm_response: str,
|
|
268
|
+
parsed_result: Dict,
|
|
269
|
+
):
|
|
270
|
+
"""
|
|
271
|
+
保存 Agent 的完整执行 trace,包括 LLM 的输入输出
|
|
272
|
+
|
|
273
|
+
文件保存在: traces/{agent_type}/{timestamp}_{task_id}.md
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
output_path: 项目输出目录 Path
|
|
277
|
+
agent_type: Agent 类型 (backend/frontend/test/audit)
|
|
278
|
+
task_id: 任务 ID
|
|
279
|
+
system_prompt: 系统提示词
|
|
280
|
+
user_prompt: 发送给 LLM 的用户提示词(完整)
|
|
281
|
+
llm_response: LLM 的原始返回
|
|
282
|
+
parsed_result: 解析后的操作结果(文件列表、契约等)
|
|
283
|
+
"""
|
|
284
|
+
traces_dir = output_path / "traces" / agent_type
|
|
285
|
+
traces_dir.mkdir(parents=True, exist_ok=True)
|
|
286
|
+
|
|
287
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
288
|
+
filename = f"{timestamp}_{task_id}.md"
|
|
289
|
+
file_path = traces_dir / filename
|
|
290
|
+
|
|
291
|
+
# 解析结果格式化
|
|
292
|
+
parsed_lines = []
|
|
293
|
+
if parsed_result.get("files"):
|
|
294
|
+
parsed_lines.append("### 生成/修改的文件")
|
|
295
|
+
for f in parsed_result["files"]:
|
|
296
|
+
parsed_lines.append(f"- `{f}`")
|
|
297
|
+
parsed_lines.append("")
|
|
298
|
+
if parsed_result.get("contracts"):
|
|
299
|
+
parsed_lines.append("### 提取的 API 契约")
|
|
300
|
+
for c in parsed_result["contracts"]:
|
|
301
|
+
parsed_lines.append(
|
|
302
|
+
f"- **{c.get('method', '?')} {c.get('endpoint', '?')}**"
|
|
303
|
+
)
|
|
304
|
+
parsed_lines.append("")
|
|
305
|
+
if parsed_result.get("reports_count") is not None:
|
|
306
|
+
parsed_lines.append(
|
|
307
|
+
f"- 发现问题数: {parsed_result.get('reports_count', 0)}"
|
|
308
|
+
)
|
|
309
|
+
parsed_lines.append(f"- 高危问题: {parsed_result.get('high_severity', 0)}")
|
|
310
|
+
parsed_lines.append("")
|
|
311
|
+
if parsed_result.get("status"):
|
|
312
|
+
parsed_lines.append(f"- 状态: {parsed_result['status']}")
|
|
313
|
+
parsed_lines.append("")
|
|
314
|
+
|
|
315
|
+
content = f"""# Agent Trace: {agent_type}
|
|
316
|
+
|
|
317
|
+
- **时间**: {datetime.now().isoformat()}
|
|
318
|
+
- **任务ID**: {task_id}
|
|
319
|
+
- **Agent 类型**: {agent_type}
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 系统提示词 (System Prompt)
|
|
324
|
+
|
|
325
|
+
{system_prompt}
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 用户提示词 (User Prompt, sent to LLM)
|
|
330
|
+
|
|
331
|
+
{user_prompt}
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## LLM 原始输出 (Raw Response)
|
|
336
|
+
|
|
337
|
+
{llm_response}
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## 解析结果 (Parsed Result)
|
|
342
|
+
|
|
343
|
+
{chr(10).join(parsed_lines) if parsed_lines else '(无解析结果)'}
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 文件写入记录
|
|
348
|
+
|
|
349
|
+
"""
|
|
350
|
+
file_path.write_text(content, encoding="utf-8")
|
|
351
|
+
|
|
352
|
+
return file_path
|
|
353
|
+
|
|
354
|
+
# ------------------------------------------------------------------ #
|
|
355
|
+
# 元数据读写
|
|
356
|
+
# ------------------------------------------------------------------ #
|
|
357
|
+
|
|
358
|
+
def update_meta(self, output_path: Path, **kwargs):
|
|
359
|
+
"""更新元数据字段"""
|
|
360
|
+
meta = self._read_meta(output_path)
|
|
361
|
+
meta.update(kwargs)
|
|
362
|
+
meta["updated_at"] = datetime.now().isoformat()
|
|
363
|
+
self._write_meta(output_path, meta)
|
|
364
|
+
|
|
365
|
+
def _read_meta(self, output_path: Path) -> Dict:
|
|
366
|
+
"""读取 .meta.json"""
|
|
367
|
+
meta_path = output_path / ".meta.json"
|
|
368
|
+
if not meta_path.exists():
|
|
369
|
+
return {}
|
|
370
|
+
return json.loads(meta_path.read_text(encoding="utf-8"))
|
|
371
|
+
|
|
372
|
+
def _write_meta(self, output_path: Path, meta: Dict):
|
|
373
|
+
"""写入 .meta.json"""
|
|
374
|
+
meta_path = output_path / ".meta.json"
|
|
375
|
+
meta_path.write_text(
|
|
376
|
+
json.dumps(meta, ensure_ascii=False, indent=2),
|
|
377
|
+
encoding="utf-8",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# ------------------------------------------------------------------ #
|
|
381
|
+
# 自动生成 README.md
|
|
382
|
+
# ------------------------------------------------------------------ #
|
|
383
|
+
|
|
384
|
+
def generate_readme(self, output_path: Path, task_results: Optional[Dict] = None):
|
|
385
|
+
"""
|
|
386
|
+
在输出目录下自动生成 README.md,包含各组件的运行指令。
|
|
387
|
+
|
|
388
|
+
根据实际生成的文件类型和内容,动态组装各章节:
|
|
389
|
+
- 后端运行指令(自动检测 Python/Node.js 等)
|
|
390
|
+
- 前端运行指令(自动检测 React/Vue 等框架)
|
|
391
|
+
- 测试运行指令
|
|
392
|
+
- 审计报告摘要
|
|
393
|
+
- Agent trace 说明
|
|
394
|
+
|
|
395
|
+
生成前会自动补充缺失的工程配置框架文件(package.json、tsconfig.json 等),
|
|
396
|
+
确保 README 中的指令可实际执行。
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
output_path: 项目输出目录 Path
|
|
400
|
+
task_results: 任务执行结果(可选),用于填充状态信息
|
|
401
|
+
"""
|
|
402
|
+
# 1. 先检测技术栈,发现缺失的配置文件
|
|
403
|
+
stack = self._detect_tech_stack(output_path)
|
|
404
|
+
|
|
405
|
+
# 2. 自动补充缺失的工程配置文件
|
|
406
|
+
generated_configs = self._auto_generate_missing_configs(output_path, stack)
|
|
407
|
+
if generated_configs:
|
|
408
|
+
# 重新检测(配置文件已补充)
|
|
409
|
+
stack = self._detect_tech_stack(output_path)
|
|
410
|
+
|
|
411
|
+
# 3. 构建 README
|
|
412
|
+
meta = self._read_meta(output_path)
|
|
413
|
+
lines = self._build_readme(output_path, meta, task_results or {})
|
|
414
|
+
readme_path = output_path / "README.md"
|
|
415
|
+
readme_path.write_text("\n".join(lines), encoding="utf-8")
|
|
416
|
+
|
|
417
|
+
def _build_readme(
|
|
418
|
+
self,
|
|
419
|
+
output_path: Path,
|
|
420
|
+
meta: Dict,
|
|
421
|
+
task_results: Dict,
|
|
422
|
+
) -> List[str]:
|
|
423
|
+
"""构建 README.md 内容,返回行列表"""
|
|
424
|
+
description = meta.get("task_description", "未命名项目")
|
|
425
|
+
created_at = meta.get("created_at", "")
|
|
426
|
+
task_id = meta.get("last_task_id", "")
|
|
427
|
+
|
|
428
|
+
lines = [
|
|
429
|
+
f"# {description}",
|
|
430
|
+
"",
|
|
431
|
+
f"> **生成时间**: {created_at[:19] if created_at else 'N/A'} ",
|
|
432
|
+
f"> **任务ID**: {task_id} ",
|
|
433
|
+
f"> **状态**: ✅ {meta.get('status', 'completed')} ",
|
|
434
|
+
"",
|
|
435
|
+
"---",
|
|
436
|
+
"",
|
|
437
|
+
"## 项目概览",
|
|
438
|
+
"",
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
# 技术栈检测
|
|
442
|
+
tech_stack = self._detect_tech_stack(output_path)
|
|
443
|
+
lines += self._format_tech_table(tech_stack)
|
|
444
|
+
|
|
445
|
+
# 目录结构
|
|
446
|
+
lines += [
|
|
447
|
+
"",
|
|
448
|
+
"## 目录结构",
|
|
449
|
+
"",
|
|
450
|
+
"```",
|
|
451
|
+
]
|
|
452
|
+
lines += self._build_tree(output_path, prefix="")
|
|
453
|
+
lines += [
|
|
454
|
+
"```",
|
|
455
|
+
"",
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
# 后端运行指令
|
|
459
|
+
backend_section = self._build_backend_section(output_path, tech_stack)
|
|
460
|
+
if backend_section:
|
|
461
|
+
lines += backend_section
|
|
462
|
+
|
|
463
|
+
# 前端运行指令
|
|
464
|
+
frontend_section = self._build_frontend_section(output_path, tech_stack)
|
|
465
|
+
if frontend_section:
|
|
466
|
+
lines += frontend_section
|
|
467
|
+
|
|
468
|
+
# 测试运行指令
|
|
469
|
+
test_section = self._build_test_section(output_path, tech_stack)
|
|
470
|
+
if test_section:
|
|
471
|
+
lines += test_section
|
|
472
|
+
|
|
473
|
+
# 审计报告
|
|
474
|
+
audit_section = self._build_audit_section(output_path)
|
|
475
|
+
if audit_section:
|
|
476
|
+
lines += audit_section
|
|
477
|
+
|
|
478
|
+
# Agent traces
|
|
479
|
+
trace_section = self._build_trace_section(output_path)
|
|
480
|
+
if trace_section:
|
|
481
|
+
lines += trace_section
|
|
482
|
+
|
|
483
|
+
# 快速验证清单
|
|
484
|
+
lines += self._build_quick_check(output_path, tech_stack)
|
|
485
|
+
|
|
486
|
+
return lines
|
|
487
|
+
|
|
488
|
+
# ---------------------------------------------------------------- #
|
|
489
|
+
# 技术栈检测
|
|
490
|
+
# ---------------------------------------------------------------- #
|
|
491
|
+
|
|
492
|
+
def _detect_tech_stack(self, output_path: Path) -> Dict:
|
|
493
|
+
"""
|
|
494
|
+
检测项目的技术栈
|
|
495
|
+
|
|
496
|
+
扫描 backend/ 和 frontend/ 目录,识别使用的语言和框架。
|
|
497
|
+
"""
|
|
498
|
+
stack = {
|
|
499
|
+
"backend_lang": None,
|
|
500
|
+
"backend_framework": None,
|
|
501
|
+
"backend_entry": None,
|
|
502
|
+
"frontend_framework": None,
|
|
503
|
+
"frontend_pkg_mgr": "npm",
|
|
504
|
+
"frontend_missing_config": False, # 有源码但缺 package.json
|
|
505
|
+
"test_backend_framework": None,
|
|
506
|
+
"test_frontend_framework": None,
|
|
507
|
+
"has_backend": False,
|
|
508
|
+
"has_frontend": False,
|
|
509
|
+
"has_tests": False,
|
|
510
|
+
"has_audits": False,
|
|
511
|
+
"has_traces": False,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# 检测后端
|
|
515
|
+
backend_dir = output_path / "backend"
|
|
516
|
+
if backend_dir.exists():
|
|
517
|
+
py_files = list(backend_dir.rglob("*.py"))
|
|
518
|
+
if py_files:
|
|
519
|
+
stack["has_backend"] = True
|
|
520
|
+
stack["backend_lang"] = "python"
|
|
521
|
+
stack["backend_framework"] = self._detect_py_framework(backend_dir)
|
|
522
|
+
stack["backend_entry"] = self._detect_entry_file(backend_dir, [".py"])
|
|
523
|
+
else:
|
|
524
|
+
js_files = list(backend_dir.rglob("*.js")) + list(
|
|
525
|
+
backend_dir.rglob("*.ts")
|
|
526
|
+
)
|
|
527
|
+
if js_files:
|
|
528
|
+
stack["has_backend"] = True
|
|
529
|
+
stack["backend_lang"] = "node"
|
|
530
|
+
stack["backend_entry"] = self._detect_entry_file(
|
|
531
|
+
backend_dir, [".js", ".ts"]
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# 检测前端——有 package.json 或有源码文件都算有前端
|
|
535
|
+
frontend_dir = output_path / "frontend"
|
|
536
|
+
if frontend_dir.exists():
|
|
537
|
+
pkg_json = frontend_dir / "package.json"
|
|
538
|
+
has_config = pkg_json.exists()
|
|
539
|
+
has_source = bool(
|
|
540
|
+
list(frontend_dir.rglob("*.tsx"))
|
|
541
|
+
+ list(frontend_dir.rglob("*.ts"))
|
|
542
|
+
+ list(frontend_dir.rglob("*.jsx"))
|
|
543
|
+
+ list(frontend_dir.rglob("*.js"))
|
|
544
|
+
+ list(frontend_dir.rglob("*.html"))
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if has_config or has_source:
|
|
548
|
+
stack["has_frontend"] = True
|
|
549
|
+
|
|
550
|
+
if has_config:
|
|
551
|
+
stack["frontend_framework"] = self._detect_frontend_framework(pkg_json)
|
|
552
|
+
# 检测包管理器
|
|
553
|
+
if (frontend_dir / "yarn.lock").exists():
|
|
554
|
+
stack["frontend_pkg_mgr"] = "yarn"
|
|
555
|
+
elif (frontend_dir / "pnpm-lock.yaml").exists():
|
|
556
|
+
stack["frontend_pkg_mgr"] = "pnpm"
|
|
557
|
+
elif has_source:
|
|
558
|
+
# 有源码但缺配置文件
|
|
559
|
+
stack["frontend_missing_config"] = True
|
|
560
|
+
# 尝试从源码中推断框架
|
|
561
|
+
for f in frontend_dir.rglob("*.tsx"):
|
|
562
|
+
try:
|
|
563
|
+
c = f.read_text(encoding="utf-8", errors="replace")
|
|
564
|
+
except Exception:
|
|
565
|
+
continue
|
|
566
|
+
if "react" in c.lower() or "jsx" in c:
|
|
567
|
+
stack["frontend_framework"] = "React"
|
|
568
|
+
break
|
|
569
|
+
if not stack["frontend_framework"]:
|
|
570
|
+
for f in frontend_dir.rglob("*.vue"):
|
|
571
|
+
stack["frontend_framework"] = "Vue"
|
|
572
|
+
break
|
|
573
|
+
if not stack["frontend_framework"]:
|
|
574
|
+
stack["frontend_framework"] = "Unknown"
|
|
575
|
+
|
|
576
|
+
# 检测测试
|
|
577
|
+
tests_dir = output_path / "tests"
|
|
578
|
+
if tests_dir.exists() and list(tests_dir.iterdir()):
|
|
579
|
+
stack["has_tests"] = True
|
|
580
|
+
for f in tests_dir.rglob("*.py"):
|
|
581
|
+
stack["test_backend_framework"] = "pytest"
|
|
582
|
+
break
|
|
583
|
+
# 只有当前端有 package.json 或测试目录下有 jest 配置时才认为有前端测试框架
|
|
584
|
+
if has_config or (tests_dir / "jest.config.js").exists():
|
|
585
|
+
for f in tests_dir.rglob("*.tsx"):
|
|
586
|
+
stack["test_frontend_framework"] = "jest"
|
|
587
|
+
break
|
|
588
|
+
|
|
589
|
+
# 检测审计
|
|
590
|
+
audits_dir = output_path / "audits"
|
|
591
|
+
if audits_dir.exists() and list(audits_dir.glob("*.md")):
|
|
592
|
+
stack["has_audits"] = True
|
|
593
|
+
|
|
594
|
+
# 检测 traces
|
|
595
|
+
traces_dir = output_path / "traces"
|
|
596
|
+
if traces_dir.exists() and list(traces_dir.rglob("*.md")):
|
|
597
|
+
stack["has_traces"] = True
|
|
598
|
+
|
|
599
|
+
return stack
|
|
600
|
+
|
|
601
|
+
@staticmethod
|
|
602
|
+
def _detect_py_framework(backend_dir: Path) -> str:
|
|
603
|
+
"""检测 Python Web 框架"""
|
|
604
|
+
for f in backend_dir.rglob("*.py"):
|
|
605
|
+
try:
|
|
606
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
607
|
+
except Exception:
|
|
608
|
+
continue
|
|
609
|
+
if (
|
|
610
|
+
"from flask" in content
|
|
611
|
+
or "import flask" in content
|
|
612
|
+
or "Flask(" in content
|
|
613
|
+
):
|
|
614
|
+
return "Flask"
|
|
615
|
+
if (
|
|
616
|
+
"from fastapi" in content
|
|
617
|
+
or "import fastapi" in content
|
|
618
|
+
or "FastAPI(" in content
|
|
619
|
+
):
|
|
620
|
+
return "FastAPI"
|
|
621
|
+
if "from django" in content or "import django" in content:
|
|
622
|
+
return "Django"
|
|
623
|
+
return "Python"
|
|
624
|
+
|
|
625
|
+
@staticmethod
|
|
626
|
+
def _detect_frontend_framework(pkg_json: Path) -> str:
|
|
627
|
+
"""从 package.json 检测前端框架"""
|
|
628
|
+
try:
|
|
629
|
+
import json
|
|
630
|
+
|
|
631
|
+
data = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
632
|
+
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
633
|
+
if "react" in deps:
|
|
634
|
+
if "next" in deps:
|
|
635
|
+
return "Next.js"
|
|
636
|
+
return "React"
|
|
637
|
+
if "vue" in deps:
|
|
638
|
+
return "Vue"
|
|
639
|
+
if "@angular/core" in deps:
|
|
640
|
+
return "Angular"
|
|
641
|
+
return "Node.js"
|
|
642
|
+
except Exception:
|
|
643
|
+
return "Unknown"
|
|
644
|
+
|
|
645
|
+
@staticmethod
|
|
646
|
+
def _detect_entry_file(directory: Path, extensions: List[str]) -> Optional[str]:
|
|
647
|
+
"""检测入口文件(main.py / app.py / index.js 等)"""
|
|
648
|
+
candidates = ["main", "app", "index", "server", "run"]
|
|
649
|
+
for name in candidates:
|
|
650
|
+
for ext in extensions:
|
|
651
|
+
f = directory / f"{name}{ext}"
|
|
652
|
+
if f.exists():
|
|
653
|
+
return f.name
|
|
654
|
+
# fallback: 取第一个匹配文件
|
|
655
|
+
for ext in extensions:
|
|
656
|
+
files = list(directory.glob(f"*{ext}"))
|
|
657
|
+
if files:
|
|
658
|
+
return files[0].name
|
|
659
|
+
return None
|
|
660
|
+
|
|
661
|
+
# ---------------------------------------------------------------- #
|
|
662
|
+
# 自动补充缺失的工程配置文件
|
|
663
|
+
# ---------------------------------------------------------------- #
|
|
664
|
+
|
|
665
|
+
def _auto_generate_missing_configs(
|
|
666
|
+
self, output_path: Path, stack: Dict
|
|
667
|
+
) -> List[str]:
|
|
668
|
+
"""
|
|
669
|
+
检查并自动生成缺失的工程配置文件。
|
|
670
|
+
|
|
671
|
+
当 Agent 生成的前端代码缺少 package.json、tsconfig.json 等配置文件时,
|
|
672
|
+
自动创建模板文件使项目可运行。
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
生成的文件路径列表
|
|
676
|
+
"""
|
|
677
|
+
generated = []
|
|
678
|
+
|
|
679
|
+
# 前端配置文件补充
|
|
680
|
+
if stack.get("frontend_missing_config"):
|
|
681
|
+
frontend_dir = output_path / "frontend"
|
|
682
|
+
pkg_path = frontend_dir / "package.json"
|
|
683
|
+
if not pkg_path.exists():
|
|
684
|
+
self._generate_template_package_json(frontend_dir, stack)
|
|
685
|
+
generated.append("frontend/package.json")
|
|
686
|
+
|
|
687
|
+
tsconfig_path = frontend_dir / "tsconfig.json"
|
|
688
|
+
if not tsconfig_path.exists():
|
|
689
|
+
self._generate_template_tsconfig_json(frontend_dir)
|
|
690
|
+
generated.append("frontend/tsconfig.json")
|
|
691
|
+
|
|
692
|
+
# 检查是否有 src/index.tsx 作为入口
|
|
693
|
+
has_entry = (frontend_dir / "src" / "index.tsx").exists() or (
|
|
694
|
+
frontend_dir / "index.tsx"
|
|
695
|
+
).exists()
|
|
696
|
+
public_html = frontend_dir / "public" / "index.html"
|
|
697
|
+
if has_entry and not public_html.exists():
|
|
698
|
+
self._generate_template_html(frontend_dir)
|
|
699
|
+
generated.append("frontend/public/index.html")
|
|
700
|
+
|
|
701
|
+
# Jest 配置补充(测试目录有 .tsx 但前端根目录缺 jest.config.js)
|
|
702
|
+
tests_dir = output_path / "tests"
|
|
703
|
+
frontend_dir = output_path / "frontend"
|
|
704
|
+
if tests_dir.exists() and list(tests_dir.rglob("*.tsx")):
|
|
705
|
+
jest_config = frontend_dir / "jest.config.js"
|
|
706
|
+
if not jest_config.exists() and (frontend_dir / "package.json").exists():
|
|
707
|
+
self._generate_template_jest_config(frontend_dir)
|
|
708
|
+
generated.append("frontend/jest.config.js")
|
|
709
|
+
setup_tests = frontend_dir / "src" / "setupTests.ts"
|
|
710
|
+
if not setup_tests.exists() and (frontend_dir / "src").exists():
|
|
711
|
+
self._generate_template_setup_tests(frontend_dir)
|
|
712
|
+
generated.append("frontend/src/setupTests.ts")
|
|
713
|
+
|
|
714
|
+
return generated
|
|
715
|
+
|
|
716
|
+
@staticmethod
|
|
717
|
+
def _generate_template_package_json(frontend_dir: Path, stack: Dict):
|
|
718
|
+
"""生成 React + TypeScript 项目的 package.json 模板"""
|
|
719
|
+
framework = stack.get("frontend_framework", "React")
|
|
720
|
+
import json
|
|
721
|
+
|
|
722
|
+
pkg = {
|
|
723
|
+
"name": "frontend",
|
|
724
|
+
"version": "1.0.0",
|
|
725
|
+
"private": True,
|
|
726
|
+
"scripts": {
|
|
727
|
+
"start": "react-scripts start",
|
|
728
|
+
"build": "react-scripts build",
|
|
729
|
+
"test": "react-scripts test",
|
|
730
|
+
"eject": "react-scripts eject",
|
|
731
|
+
},
|
|
732
|
+
"dependencies": {
|
|
733
|
+
"react": "^18.2.0",
|
|
734
|
+
"react-dom": "^18.2.0",
|
|
735
|
+
"react-scripts": "5.0.1",
|
|
736
|
+
},
|
|
737
|
+
"devDependencies": {
|
|
738
|
+
"@testing-library/jest-dom": "^5.16.5",
|
|
739
|
+
"@testing-library/react": "^14.0.0",
|
|
740
|
+
"@testing-library/user-event": "^14.4.3",
|
|
741
|
+
"@types/jest": "^29.5.0",
|
|
742
|
+
"@types/node": "^20.1.0",
|
|
743
|
+
"@types/react": "^18.2.0",
|
|
744
|
+
"@types/react-dom": "^18.2.0",
|
|
745
|
+
"typescript": "^4.9.5",
|
|
746
|
+
"web-vitals": "^3.3.0",
|
|
747
|
+
},
|
|
748
|
+
"browserslist": {
|
|
749
|
+
"production": [">0.2%", "not dead", "not op_mini all"],
|
|
750
|
+
"development": [
|
|
751
|
+
"last 1 chrome version",
|
|
752
|
+
"last 1 firefox version",
|
|
753
|
+
"last 1 safari version",
|
|
754
|
+
],
|
|
755
|
+
},
|
|
756
|
+
"homepage": ".",
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if framework == "Vue":
|
|
760
|
+
pkg["dependencies"] = {"vue": "^3.3.0"}
|
|
761
|
+
pkg["devDependencies"] = {
|
|
762
|
+
"@vitejs/plugin-vue": "^4.2.0",
|
|
763
|
+
"vite": "^4.3.0",
|
|
764
|
+
"typescript": "^5.0.0",
|
|
765
|
+
}
|
|
766
|
+
pkg["scripts"] = {
|
|
767
|
+
"dev": "vite",
|
|
768
|
+
"build": "vite build",
|
|
769
|
+
"preview": "vite preview",
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
pkg_path = frontend_dir / "package.json"
|
|
773
|
+
pkg_path.write_text(
|
|
774
|
+
json.dumps(pkg, indent=2, ensure_ascii=False),
|
|
775
|
+
encoding="utf-8",
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
@staticmethod
|
|
779
|
+
def _generate_template_tsconfig_json(frontend_dir: Path):
|
|
780
|
+
"""生成 TypeScript 配置模板"""
|
|
781
|
+
import json
|
|
782
|
+
|
|
783
|
+
tsconfig = {
|
|
784
|
+
"compilerOptions": {
|
|
785
|
+
"target": "ES2020",
|
|
786
|
+
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
787
|
+
"allowJs": True,
|
|
788
|
+
"skipLibCheck": True,
|
|
789
|
+
"esModuleInterop": True,
|
|
790
|
+
"allowSyntheticDefaultImports": True,
|
|
791
|
+
"strict": True,
|
|
792
|
+
"forceConsistentCasingInFileNames": True,
|
|
793
|
+
"noFallthroughCasesInSwitch": True,
|
|
794
|
+
"module": "ESNext",
|
|
795
|
+
"moduleResolution": "node",
|
|
796
|
+
"resolveJsonModule": True,
|
|
797
|
+
"isolatedModules": True,
|
|
798
|
+
"noEmit": True,
|
|
799
|
+
"jsx": "react-jsx",
|
|
800
|
+
},
|
|
801
|
+
"include": ["src", "*.tsx", "*.ts"],
|
|
802
|
+
}
|
|
803
|
+
(frontend_dir / "tsconfig.json").write_text(
|
|
804
|
+
json.dumps(tsconfig, indent=2, ensure_ascii=False),
|
|
805
|
+
encoding="utf-8",
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
@staticmethod
|
|
809
|
+
def _generate_template_html(frontend_dir: Path):
|
|
810
|
+
"""生成 React 入口 HTML 模板"""
|
|
811
|
+
html = """<!DOCTYPE html>
|
|
812
|
+
<html lang="zh-CN">
|
|
813
|
+
<head>
|
|
814
|
+
<meta charset="utf-8" />
|
|
815
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
816
|
+
<meta name="theme-color" content="#000000" />
|
|
817
|
+
<meta name="description" content="AI-generated web application" />
|
|
818
|
+
<title>App</title>
|
|
819
|
+
</head>
|
|
820
|
+
<body>
|
|
821
|
+
<noscript>您需要启用 JavaScript 才能运行此应用。</noscript>
|
|
822
|
+
<div id="root"></div>
|
|
823
|
+
</body>
|
|
824
|
+
</html>
|
|
825
|
+
"""
|
|
826
|
+
public_dir = frontend_dir / "public"
|
|
827
|
+
public_dir.mkdir(parents=True, exist_ok=True)
|
|
828
|
+
(public_dir / "index.html").write_text(html, encoding="utf-8")
|
|
829
|
+
|
|
830
|
+
@staticmethod
|
|
831
|
+
def _generate_template_jest_config(frontend_dir: Path):
|
|
832
|
+
"""生成 Jest 配置模板"""
|
|
833
|
+
config = """module.exports = {
|
|
834
|
+
testEnvironment: 'jsdom',
|
|
835
|
+
setupFilesAfterSetup: ['<rootDir>/src/setupTests.ts'],
|
|
836
|
+
moduleNameMapper: {
|
|
837
|
+
'\\\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
"""
|
|
841
|
+
(frontend_dir / "jest.config.js").write_text(config, encoding="utf-8")
|
|
842
|
+
|
|
843
|
+
@staticmethod
|
|
844
|
+
def _generate_template_setup_tests(frontend_dir: Path):
|
|
845
|
+
"""生成 Jest setup 文件"""
|
|
846
|
+
content = "import '@testing-library/jest-dom';\n"
|
|
847
|
+
src_dir = frontend_dir / "src"
|
|
848
|
+
src_dir.mkdir(parents=True, exist_ok=True)
|
|
849
|
+
(src_dir / "setupTests.ts").write_text(content, encoding="utf-8")
|
|
850
|
+
|
|
851
|
+
# ---------------------------------------------------------------- #
|
|
852
|
+
# 各章节生成
|
|
853
|
+
# ---------------------------------------------------------------- #
|
|
854
|
+
|
|
855
|
+
def _format_tech_table(self, stack: Dict) -> List[str]:
|
|
856
|
+
"""格式化成技术栈表格"""
|
|
857
|
+
lines = ["| 组件 | 技术栈 | 目录 |", "|------|--------|------|"]
|
|
858
|
+
if stack["has_backend"]:
|
|
859
|
+
framework = stack["backend_framework"] or stack["backend_lang"] or "—"
|
|
860
|
+
lines.append(f"| 后端 API | {framework} | `backend/` |")
|
|
861
|
+
if stack["has_frontend"]:
|
|
862
|
+
framework = stack["frontend_framework"] or "—"
|
|
863
|
+
note = " ⚠️缺配置" if stack.get("frontend_missing_config") else ""
|
|
864
|
+
lines.append(f"| 前端界面 | {framework}{note} | `frontend/` |")
|
|
865
|
+
if stack["has_tests"]:
|
|
866
|
+
test_info = []
|
|
867
|
+
if stack["test_backend_framework"]:
|
|
868
|
+
test_info.append(stack["test_backend_framework"])
|
|
869
|
+
if stack["test_frontend_framework"]:
|
|
870
|
+
test_info.append(stack["test_frontend_framework"])
|
|
871
|
+
lines.append(f"| 测试 | {', '.join(test_info) or '—'} | `tests/` |")
|
|
872
|
+
if stack["has_audits"]:
|
|
873
|
+
lines.append("| 代码审计 | AI 自动审计 | `audits/` |")
|
|
874
|
+
if stack["has_traces"]:
|
|
875
|
+
lines.append("| 执行追溯 | Agent I/O | `traces/` |")
|
|
876
|
+
return lines
|
|
877
|
+
|
|
878
|
+
def _build_tree(
|
|
879
|
+
self, path: Path, prefix: str = "", max_depth: int = 3, _depth: int = 0
|
|
880
|
+
) -> List[str]:
|
|
881
|
+
"""生成目录树"""
|
|
882
|
+
if _depth >= max_depth:
|
|
883
|
+
return []
|
|
884
|
+
lines = []
|
|
885
|
+
entries = sorted(
|
|
886
|
+
[e for e in path.iterdir() if not e.name.startswith(".")],
|
|
887
|
+
key=lambda e: (e.is_file(), e.name),
|
|
888
|
+
)
|
|
889
|
+
for i, entry in enumerate(entries):
|
|
890
|
+
is_last = i == len(entries) - 1
|
|
891
|
+
connector = "└── " if is_last else "├── "
|
|
892
|
+
if entry.is_dir():
|
|
893
|
+
lines.append(f"{prefix}{connector}{entry.name}/")
|
|
894
|
+
sub_prefix = prefix + (" " if is_last else "│ ")
|
|
895
|
+
lines += self._build_tree(entry, sub_prefix, max_depth, _depth + 1)
|
|
896
|
+
else:
|
|
897
|
+
lines.append(f"{prefix}{connector}{entry.name}")
|
|
898
|
+
return lines
|
|
899
|
+
|
|
900
|
+
def _build_backend_section(self, output_path: Path, stack: Dict) -> List[str]:
|
|
901
|
+
"""生成后端运行章节"""
|
|
902
|
+
if not stack["has_backend"]:
|
|
903
|
+
return []
|
|
904
|
+
|
|
905
|
+
backend_dir = output_path / "backend"
|
|
906
|
+
lines = [
|
|
907
|
+
"",
|
|
908
|
+
"## 一、运行后端 (Backend)",
|
|
909
|
+
"",
|
|
910
|
+
]
|
|
911
|
+
|
|
912
|
+
entry = stack["backend_entry"]
|
|
913
|
+
|
|
914
|
+
if stack["backend_lang"] == "python":
|
|
915
|
+
# 收集依赖
|
|
916
|
+
deps = self._collect_py_deps(backend_dir)
|
|
917
|
+
lines += [
|
|
918
|
+
"### 1.1 安装依赖",
|
|
919
|
+
"",
|
|
920
|
+
"```bash",
|
|
921
|
+
"cd backend/",
|
|
922
|
+
]
|
|
923
|
+
if deps:
|
|
924
|
+
lines.append(f"pip install {' '.join(deps)}")
|
|
925
|
+
else:
|
|
926
|
+
lines.append("# 如有 requirements.txt: pip install -r requirements.txt")
|
|
927
|
+
lines += [
|
|
928
|
+
"```",
|
|
929
|
+
"",
|
|
930
|
+
"### 1.2 启动服务",
|
|
931
|
+
"",
|
|
932
|
+
"```bash",
|
|
933
|
+
]
|
|
934
|
+
if entry:
|
|
935
|
+
lines.append(f"python {entry}")
|
|
936
|
+
else:
|
|
937
|
+
lines.append("python main.py")
|
|
938
|
+
lines += [
|
|
939
|
+
"```",
|
|
940
|
+
"",
|
|
941
|
+
]
|
|
942
|
+
|
|
943
|
+
# 提取 API 契约
|
|
944
|
+
contracts = self._extract_api_contracts(backend_dir)
|
|
945
|
+
if contracts:
|
|
946
|
+
lines += [
|
|
947
|
+
"### 1.3 API 契约",
|
|
948
|
+
"",
|
|
949
|
+
"| 方法 | 端点 | 请求体 | 响应体 |",
|
|
950
|
+
"|------|------|--------|--------|",
|
|
951
|
+
]
|
|
952
|
+
for c in contracts:
|
|
953
|
+
method = c.get("method", "?")
|
|
954
|
+
endpoint = c.get("endpoint", "?")
|
|
955
|
+
req = c.get("request_schema", "—")
|
|
956
|
+
resp = c.get("response_schema", "—")
|
|
957
|
+
lines.append(f"| {method} | `{endpoint}` | `{req}` | `{resp}` |")
|
|
958
|
+
|
|
959
|
+
# 生成 curl 示例
|
|
960
|
+
lines += [
|
|
961
|
+
"",
|
|
962
|
+
"### 1.4 验证 API",
|
|
963
|
+
"",
|
|
964
|
+
]
|
|
965
|
+
for c in contracts[:1]: # 只生成第一个端点的示例
|
|
966
|
+
method = c.get("method", "GET")
|
|
967
|
+
endpoint = c.get("endpoint", "/")
|
|
968
|
+
req_schema = c.get("request_schema", "")
|
|
969
|
+
# 简单解析 JSON schema 生成 curl
|
|
970
|
+
curl = self._gen_curl_example(method, endpoint, req_schema)
|
|
971
|
+
lines += curl
|
|
972
|
+
lines.append("")
|
|
973
|
+
|
|
974
|
+
elif stack["backend_lang"] == "node":
|
|
975
|
+
lines += [
|
|
976
|
+
"### 1.1 安装依赖",
|
|
977
|
+
"",
|
|
978
|
+
"```bash",
|
|
979
|
+
"cd backend/",
|
|
980
|
+
f"{stack['frontend_pkg_mgr']} install",
|
|
981
|
+
"```",
|
|
982
|
+
"",
|
|
983
|
+
"### 1.2 启动服务",
|
|
984
|
+
"",
|
|
985
|
+
"```bash",
|
|
986
|
+
f"{stack['frontend_pkg_mgr']} start",
|
|
987
|
+
"```",
|
|
988
|
+
"",
|
|
989
|
+
]
|
|
990
|
+
|
|
991
|
+
return lines
|
|
992
|
+
|
|
993
|
+
def _build_frontend_section(self, output_path: Path, stack: Dict) -> List[str]:
|
|
994
|
+
"""生成前端运行章节"""
|
|
995
|
+
if not stack["has_frontend"]:
|
|
996
|
+
return []
|
|
997
|
+
|
|
998
|
+
frontend_dir = output_path / "frontend"
|
|
999
|
+
lines = [
|
|
1000
|
+
"",
|
|
1001
|
+
"## 二、运行前端 (Frontend)",
|
|
1002
|
+
"",
|
|
1003
|
+
]
|
|
1004
|
+
|
|
1005
|
+
pkg_json_path = frontend_dir / "package.json"
|
|
1006
|
+
if not pkg_json_path.exists():
|
|
1007
|
+
# 有源码但缺配置文件——理论上 auto_generate 应该已经处理了,
|
|
1008
|
+
# 这里作为兜底,给出手动初始化 React 的指令
|
|
1009
|
+
lines += [
|
|
1010
|
+
"> ⚠️ **缺少工程配置文件**:前端源码存在但无 `package.json`,需要手动初始化。",
|
|
1011
|
+
"",
|
|
1012
|
+
"### 2.1 初始化前端项目",
|
|
1013
|
+
"",
|
|
1014
|
+
"```bash",
|
|
1015
|
+
"# 使用 Create React App 初始化 TypeScript 模板",
|
|
1016
|
+
"cd frontend/",
|
|
1017
|
+
"npx create-react-app . --template typescript --use-npm 2>/dev/null || \\",
|
|
1018
|
+
" npm init -y && npm install react react-dom react-scripts typescript \\",
|
|
1019
|
+
" @types/react @types/react-dom @testing-library/react @testing-library/jest-dom",
|
|
1020
|
+
"```",
|
|
1021
|
+
"",
|
|
1022
|
+
"### 2.2 将源码移入 src/ 目录",
|
|
1023
|
+
"",
|
|
1024
|
+
"```bash",
|
|
1025
|
+
"# 将组件和入口文件移入 src/(如果尚未在 src/ 中)",
|
|
1026
|
+
"find . -maxdepth 1 -name '*.tsx' -o -name '*.ts' \\",
|
|
1027
|
+
" -o -name '*.css' | grep -v 'node_modules' | xargs -I{} mv {} src/ 2>/dev/null",
|
|
1028
|
+
"```",
|
|
1029
|
+
"",
|
|
1030
|
+
"### 2.3 启动开发服务器",
|
|
1031
|
+
"",
|
|
1032
|
+
"```bash",
|
|
1033
|
+
"npm start",
|
|
1034
|
+
"```",
|
|
1035
|
+
"",
|
|
1036
|
+
]
|
|
1037
|
+
return lines
|
|
1038
|
+
|
|
1039
|
+
framework = stack["frontend_framework"]
|
|
1040
|
+
if framework:
|
|
1041
|
+
pkg_mgr = stack["frontend_pkg_mgr"]
|
|
1042
|
+
lines += [
|
|
1043
|
+
"### 2.1 安装依赖",
|
|
1044
|
+
"",
|
|
1045
|
+
"```bash",
|
|
1046
|
+
"cd frontend/",
|
|
1047
|
+
f"{pkg_mgr} install",
|
|
1048
|
+
"```",
|
|
1049
|
+
"",
|
|
1050
|
+
"### 2.2 启动开发服务器",
|
|
1051
|
+
"",
|
|
1052
|
+
"```bash",
|
|
1053
|
+
]
|
|
1054
|
+
if framework == "Next.js":
|
|
1055
|
+
lines.append(f"{pkg_mgr} run dev")
|
|
1056
|
+
else:
|
|
1057
|
+
lines.append(f"{pkg_mgr} start")
|
|
1058
|
+
lines += [
|
|
1059
|
+
"```",
|
|
1060
|
+
"",
|
|
1061
|
+
]
|
|
1062
|
+
|
|
1063
|
+
# 检测是否需要补充依赖
|
|
1064
|
+
missing = self._detect_missing_deps(frontend_dir)
|
|
1065
|
+
if missing:
|
|
1066
|
+
lines += [
|
|
1067
|
+
"> ⚠️ **可能缺少的依赖**: `package.json` 未声明但代码中引用了:",
|
|
1068
|
+
]
|
|
1069
|
+
for dep in missing:
|
|
1070
|
+
lines.append(f"> - `{dep}`")
|
|
1071
|
+
lines += [
|
|
1072
|
+
f"> 如需安装: `{pkg_mgr} install {' '.join(missing)}`",
|
|
1073
|
+
"",
|
|
1074
|
+
]
|
|
1075
|
+
else:
|
|
1076
|
+
# 纯 HTML 前端
|
|
1077
|
+
html_files = list(frontend_dir.rglob("*.html"))
|
|
1078
|
+
if html_files:
|
|
1079
|
+
entry_html = html_files[0]
|
|
1080
|
+
lines += [
|
|
1081
|
+
"可以直接用浏览器打开 HTML 文件,或使用简单 HTTP 服务器:",
|
|
1082
|
+
"",
|
|
1083
|
+
"```bash",
|
|
1084
|
+
"cd frontend/",
|
|
1085
|
+
"python -m http.server 8000",
|
|
1086
|
+
"```",
|
|
1087
|
+
f"浏览器访问: `http://localhost:8000/{entry_html.name}`",
|
|
1088
|
+
"",
|
|
1089
|
+
]
|
|
1090
|
+
|
|
1091
|
+
return lines
|
|
1092
|
+
|
|
1093
|
+
def _build_test_section(self, output_path: Path, stack: Dict) -> List[str]:
|
|
1094
|
+
"""生成测试运行章节"""
|
|
1095
|
+
if not stack["has_tests"]:
|
|
1096
|
+
return []
|
|
1097
|
+
|
|
1098
|
+
tests_dir = output_path / "tests"
|
|
1099
|
+
frontend_pkg = (output_path / "frontend" / "package.json").exists()
|
|
1100
|
+
|
|
1101
|
+
lines = [
|
|
1102
|
+
"",
|
|
1103
|
+
"## 三、运行测试 (Tests)",
|
|
1104
|
+
"",
|
|
1105
|
+
]
|
|
1106
|
+
|
|
1107
|
+
# 后端测试
|
|
1108
|
+
py_tests = list(tests_dir.rglob("*.py"))
|
|
1109
|
+
if py_tests:
|
|
1110
|
+
test_files = " ".join(f"tests/{f.name}" for f in py_tests)
|
|
1111
|
+
lines += [
|
|
1112
|
+
"### 3.1 后端测试",
|
|
1113
|
+
"",
|
|
1114
|
+
"```bash",
|
|
1115
|
+
"# 在项目根目录(本目录)下执行",
|
|
1116
|
+
f"python -m pytest {test_files} -v",
|
|
1117
|
+
"```",
|
|
1118
|
+
"",
|
|
1119
|
+
"**测试文件**:",
|
|
1120
|
+
]
|
|
1121
|
+
for f in py_tests:
|
|
1122
|
+
cases = self._extract_test_cases(f)
|
|
1123
|
+
lines.append(f"- `{f.name}`: {cases}")
|
|
1124
|
+
lines.append("")
|
|
1125
|
+
|
|
1126
|
+
# 前端测试——搜索 tests/ 和 frontend/src/ 两个目录
|
|
1127
|
+
frontend_test_candidates = (
|
|
1128
|
+
list(tests_dir.rglob("*.tsx"))
|
|
1129
|
+
+ list(tests_dir.rglob("*.ts"))
|
|
1130
|
+
+ list(tests_dir.rglob("*.jsx"))
|
|
1131
|
+
+ list(tests_dir.rglob("*.js"))
|
|
1132
|
+
)
|
|
1133
|
+
frontend_dir = output_path / "frontend"
|
|
1134
|
+
if frontend_dir.exists():
|
|
1135
|
+
frontend_test_candidates += (
|
|
1136
|
+
list(frontend_dir.rglob("*.test.tsx"))
|
|
1137
|
+
+ list(frontend_dir.rglob("*.test.ts"))
|
|
1138
|
+
+ list(frontend_dir.rglob("*.test.jsx"))
|
|
1139
|
+
+ list(frontend_dir.rglob("*.test.js"))
|
|
1140
|
+
+ list(frontend_dir.rglob("*.spec.tsx"))
|
|
1141
|
+
+ list(frontend_dir.rglob("*.spec.ts"))
|
|
1142
|
+
)
|
|
1143
|
+
js_tests = [f for f in frontend_test_candidates if f.suffix != ".py"]
|
|
1144
|
+
if js_tests:
|
|
1145
|
+
if frontend_pkg:
|
|
1146
|
+
pkg_mgr = stack["frontend_pkg_mgr"]
|
|
1147
|
+
lines += [
|
|
1148
|
+
"### 3.2 前端测试",
|
|
1149
|
+
"",
|
|
1150
|
+
"```bash",
|
|
1151
|
+
"cd frontend/",
|
|
1152
|
+
f"{pkg_mgr} test",
|
|
1153
|
+
"```",
|
|
1154
|
+
"",
|
|
1155
|
+
"**测试文件**:",
|
|
1156
|
+
]
|
|
1157
|
+
for f in js_tests:
|
|
1158
|
+
lines.append(f"- `{f.relative_to(tests_dir.parent)}`")
|
|
1159
|
+
lines.append("")
|
|
1160
|
+
else:
|
|
1161
|
+
lines += [
|
|
1162
|
+
"### 3.2 前端测试",
|
|
1163
|
+
"",
|
|
1164
|
+
"> ⚠️ **缺少 `frontend/package.json`**,前端测试无法直接运行。",
|
|
1165
|
+
"> 请先初始化前端项目(参见 README 中'运行前端'章节),然后:",
|
|
1166
|
+
"",
|
|
1167
|
+
"**测试文件**(位于 `tests/` 目录,需移到 `frontend/src/` 下运行):",
|
|
1168
|
+
]
|
|
1169
|
+
for f in js_tests:
|
|
1170
|
+
lines.append(f"- `{f.relative_to(tests_dir.parent)}`")
|
|
1171
|
+
lines.append("")
|
|
1172
|
+
|
|
1173
|
+
return lines
|
|
1174
|
+
|
|
1175
|
+
def _build_audit_section(self, output_path: Path) -> List[str]:
|
|
1176
|
+
"""生成审计章节"""
|
|
1177
|
+
audits_dir = output_path / "audits"
|
|
1178
|
+
if not audits_dir.exists():
|
|
1179
|
+
return []
|
|
1180
|
+
|
|
1181
|
+
audit_files = sorted(audits_dir.glob("*.md"))
|
|
1182
|
+
if not audit_files:
|
|
1183
|
+
return []
|
|
1184
|
+
|
|
1185
|
+
lines = [
|
|
1186
|
+
"",
|
|
1187
|
+
"## 四、代码审计报告",
|
|
1188
|
+
"",
|
|
1189
|
+
]
|
|
1190
|
+
|
|
1191
|
+
for af in audit_files:
|
|
1192
|
+
content = af.read_text(encoding="utf-8", errors="replace")
|
|
1193
|
+
# 提取发现的问题数
|
|
1194
|
+
import re as _re
|
|
1195
|
+
|
|
1196
|
+
count_match = _re.search(r"发现问题数[::]\s*(\d+)", content)
|
|
1197
|
+
issue_count = count_match.group(1) if count_match else "?"
|
|
1198
|
+
lines.append(f"- [{af.name}](audits/{af.name}) — 发现 {issue_count} 个问题")
|
|
1199
|
+
|
|
1200
|
+
lines += [
|
|
1201
|
+
"",
|
|
1202
|
+
"查看完整报告:",
|
|
1203
|
+
"",
|
|
1204
|
+
"```bash",
|
|
1205
|
+
f"cat audits/{audit_files[0].name}",
|
|
1206
|
+
"```",
|
|
1207
|
+
"",
|
|
1208
|
+
]
|
|
1209
|
+
|
|
1210
|
+
return lines
|
|
1211
|
+
|
|
1212
|
+
def _build_trace_section(self, output_path: Path) -> List[str]:
|
|
1213
|
+
"""生成 trace 章节"""
|
|
1214
|
+
traces_dir = output_path / "traces"
|
|
1215
|
+
if not traces_dir.exists():
|
|
1216
|
+
return []
|
|
1217
|
+
|
|
1218
|
+
trace_files = list(traces_dir.rglob("*.md"))
|
|
1219
|
+
if not trace_files:
|
|
1220
|
+
return []
|
|
1221
|
+
|
|
1222
|
+
lines = [
|
|
1223
|
+
"",
|
|
1224
|
+
"## 五、Agent 执行追溯",
|
|
1225
|
+
"",
|
|
1226
|
+
"每个 Agent 的完整输入输出记录在 `traces/` 下,用于审计和调试:",
|
|
1227
|
+
"",
|
|
1228
|
+
"```bash",
|
|
1229
|
+
]
|
|
1230
|
+
for sub in sorted(traces_dir.iterdir()):
|
|
1231
|
+
if sub.is_dir():
|
|
1232
|
+
count = len(list(sub.glob("*.md")))
|
|
1233
|
+
if count > 0:
|
|
1234
|
+
lines.append(
|
|
1235
|
+
f"cat traces/{sub.name}/*.md # {count} 个 trace 文件"
|
|
1236
|
+
)
|
|
1237
|
+
lines += [
|
|
1238
|
+
"```",
|
|
1239
|
+
"",
|
|
1240
|
+
]
|
|
1241
|
+
return lines
|
|
1242
|
+
|
|
1243
|
+
def _build_quick_check(self, output_path: Path, stack: Dict) -> List[str]:
|
|
1244
|
+
"""生成快速验证清单"""
|
|
1245
|
+
frontend_pkg = (output_path / "frontend" / "package.json").exists()
|
|
1246
|
+
|
|
1247
|
+
lines = [
|
|
1248
|
+
"",
|
|
1249
|
+
"## 六、快速验证清单",
|
|
1250
|
+
"",
|
|
1251
|
+
"| 步骤 | 操作 | 预期 |",
|
|
1252
|
+
"|------|------|------|",
|
|
1253
|
+
]
|
|
1254
|
+
|
|
1255
|
+
if stack["has_backend"]:
|
|
1256
|
+
entry = stack["backend_entry"] or "main.py"
|
|
1257
|
+
lines.append(f"| 后端启动 | `cd backend && python {entry}` | 服务运行中 |")
|
|
1258
|
+
# 尝试找 API 端点
|
|
1259
|
+
api_info = self._find_first_api(output_path)
|
|
1260
|
+
if api_info:
|
|
1261
|
+
lines.append(f"| API 验证 | `curl {api_info}` | 返回 200 |")
|
|
1262
|
+
|
|
1263
|
+
if stack["has_frontend"] and frontend_pkg:
|
|
1264
|
+
lines.append(
|
|
1265
|
+
f"| 前端启动 | `cd frontend && {stack['frontend_pkg_mgr']} start` | 页面可访问 |"
|
|
1266
|
+
)
|
|
1267
|
+
elif stack["has_frontend"]:
|
|
1268
|
+
lines.append("| 前端初始化 | 参见「运行前端」章节补充配置文件 | — |")
|
|
1269
|
+
|
|
1270
|
+
if stack["has_tests"] and stack["test_backend_framework"]:
|
|
1271
|
+
lines.append("| 后端测试 | `python -m pytest tests/ -v` | 全部通过 |")
|
|
1272
|
+
|
|
1273
|
+
if stack["has_tests"] and stack["test_frontend_framework"] and frontend_pkg:
|
|
1274
|
+
lines.append(
|
|
1275
|
+
f"| 前端测试 | `cd frontend && {stack['frontend_pkg_mgr']} test` | 全部通过 |"
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
if stack["has_audits"]:
|
|
1279
|
+
lines.append("| 查看审计 | `cat audits/*.md` | 含问题清单 |")
|
|
1280
|
+
|
|
1281
|
+
if stack["has_traces"]:
|
|
1282
|
+
lines.append("| 查看追溯 | `ls traces/*/` | trace 文件 |")
|
|
1283
|
+
|
|
1284
|
+
lines.append("")
|
|
1285
|
+
|
|
1286
|
+
# 端到端启动(仅当配置文件齐全时提供脚本)
|
|
1287
|
+
can_start_frontend = (
|
|
1288
|
+
stack["has_frontend"] and frontend_pkg and stack.get("frontend_framework")
|
|
1289
|
+
)
|
|
1290
|
+
if stack["has_backend"] or can_start_frontend:
|
|
1291
|
+
lines += [
|
|
1292
|
+
"---",
|
|
1293
|
+
"",
|
|
1294
|
+
"## 端到端启动",
|
|
1295
|
+
"",
|
|
1296
|
+
"```bash",
|
|
1297
|
+
f"cd {output_path.name}",
|
|
1298
|
+
"",
|
|
1299
|
+
]
|
|
1300
|
+
if stack["has_backend"] and stack["backend_lang"] == "python":
|
|
1301
|
+
entry = stack["backend_entry"] or "main.py"
|
|
1302
|
+
lines += [
|
|
1303
|
+
"# 启动后端",
|
|
1304
|
+
"cd backend/",
|
|
1305
|
+
"pip install flask marshmallow 2>/dev/null",
|
|
1306
|
+
f"python {entry} &",
|
|
1307
|
+
"BACKEND_PID=$!",
|
|
1308
|
+
"cd ..",
|
|
1309
|
+
"",
|
|
1310
|
+
]
|
|
1311
|
+
if can_start_frontend:
|
|
1312
|
+
pkg_mgr = stack["frontend_pkg_mgr"]
|
|
1313
|
+
lines += [
|
|
1314
|
+
"# 启动前端",
|
|
1315
|
+
"cd frontend/",
|
|
1316
|
+
f"{pkg_mgr} install 2>/dev/null",
|
|
1317
|
+
f"{pkg_mgr} start &",
|
|
1318
|
+
"FRONTEND_PID=$!",
|
|
1319
|
+
"cd ..",
|
|
1320
|
+
"",
|
|
1321
|
+
]
|
|
1322
|
+
lines += [
|
|
1323
|
+
'echo "后端: http://127.0.0.1:5000"',
|
|
1324
|
+
'echo "前端: http://localhost:3000"',
|
|
1325
|
+
"",
|
|
1326
|
+
"# 停止服务",
|
|
1327
|
+
"# kill $BACKEND_PID $FRONTEND_PID",
|
|
1328
|
+
"```",
|
|
1329
|
+
"",
|
|
1330
|
+
]
|
|
1331
|
+
|
|
1332
|
+
return lines
|
|
1333
|
+
|
|
1334
|
+
# ---------------------------------------------------------------- #
|
|
1335
|
+
# README 构建辅助方法
|
|
1336
|
+
# ---------------------------------------------------------------- #
|
|
1337
|
+
|
|
1338
|
+
@staticmethod
|
|
1339
|
+
def _collect_py_deps(backend_dir: Path) -> List[str]:
|
|
1340
|
+
"""从 Python 文件中收集常见的第三方 import"""
|
|
1341
|
+
common_deps = {
|
|
1342
|
+
"flask": "flask",
|
|
1343
|
+
"marshmallow": "marshmallow",
|
|
1344
|
+
"fastapi": "fastapi",
|
|
1345
|
+
"pydantic": "pydantic",
|
|
1346
|
+
"sqlalchemy": "sqlalchemy",
|
|
1347
|
+
"django": "django",
|
|
1348
|
+
"requests": "requests",
|
|
1349
|
+
"celery": "celery",
|
|
1350
|
+
"redis": "redis",
|
|
1351
|
+
"pymongo": "pymongo",
|
|
1352
|
+
"psycopg2": "psycopg2-binary",
|
|
1353
|
+
"pytest": "pytest",
|
|
1354
|
+
}
|
|
1355
|
+
found = set()
|
|
1356
|
+
for f in backend_dir.rglob("*.py"):
|
|
1357
|
+
try:
|
|
1358
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
1359
|
+
except Exception:
|
|
1360
|
+
continue
|
|
1361
|
+
for mod, pkg in common_deps.items():
|
|
1362
|
+
if f"import {mod}" in content or f"from {mod}" in content:
|
|
1363
|
+
found.add(pkg)
|
|
1364
|
+
# 读取 requirements.txt(如果存在)
|
|
1365
|
+
reqs_file = backend_dir / "requirements.txt"
|
|
1366
|
+
if reqs_file.exists():
|
|
1367
|
+
return [] # 有 requirements.txt 就让用户自己装
|
|
1368
|
+
return sorted(found) if found else []
|
|
1369
|
+
|
|
1370
|
+
@staticmethod
|
|
1371
|
+
def _extract_api_contracts(backend_dir: Path) -> List[Dict]:
|
|
1372
|
+
"""从后端代码中提取 API_CONTRACT 注释块"""
|
|
1373
|
+
contracts = []
|
|
1374
|
+
for f in backend_dir.rglob("*.py"):
|
|
1375
|
+
try:
|
|
1376
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
1377
|
+
except Exception:
|
|
1378
|
+
continue
|
|
1379
|
+
# 匹配 API_CONTRACT_START ... API_CONTRACT_END 块
|
|
1380
|
+
import re as _re
|
|
1381
|
+
|
|
1382
|
+
pattern = r"###\s*API_CONTRACT_START\s*\n(.*?)###\s*API_CONTRACT_END"
|
|
1383
|
+
for match in _re.finditer(pattern, content, _re.DOTALL):
|
|
1384
|
+
block = match.group(1)
|
|
1385
|
+
contract = {}
|
|
1386
|
+
for line in block.strip().split("\n"):
|
|
1387
|
+
line = line.strip()
|
|
1388
|
+
if line.startswith("endpoint:"):
|
|
1389
|
+
contract["endpoint"] = line.split(":", 1)[1].strip()
|
|
1390
|
+
elif line.startswith("method:"):
|
|
1391
|
+
contract["method"] = line.split(":", 1)[1].strip()
|
|
1392
|
+
elif line.startswith("request_schema:"):
|
|
1393
|
+
contract["request_schema"] = line.split(":", 1)[1].strip()
|
|
1394
|
+
elif line.startswith("response_schema:"):
|
|
1395
|
+
contract["response_schema"] = line.split(":", 1)[1].strip()
|
|
1396
|
+
if contract.get("endpoint"):
|
|
1397
|
+
contracts.append(contract)
|
|
1398
|
+
return contracts
|
|
1399
|
+
|
|
1400
|
+
@staticmethod
|
|
1401
|
+
def _gen_curl_example(method: str, endpoint: str, req_schema: str) -> List[str]:
|
|
1402
|
+
"""根据 API 契约生成 curl 示例"""
|
|
1403
|
+
lines = ["```bash"]
|
|
1404
|
+
# 生成请求体示例
|
|
1405
|
+
body = "{}"
|
|
1406
|
+
if req_schema:
|
|
1407
|
+
import re as _re
|
|
1408
|
+
|
|
1409
|
+
# 简单解析 {"key": "type"} 格式
|
|
1410
|
+
props = _re.findall(r'"(\w+)":\s*"(\w+)"', req_schema)
|
|
1411
|
+
if props:
|
|
1412
|
+
body_parts = [f'"{k}": "{v}"' for k, v in props]
|
|
1413
|
+
body = "{" + ", ".join(body_parts) + "}"
|
|
1414
|
+
if method.upper() in ("POST", "PUT", "PATCH"):
|
|
1415
|
+
lines.append(f"curl -X {method.upper()} http://127.0.0.1:5000{endpoint} \\")
|
|
1416
|
+
lines.append(f' -H "Content-Type: application/json" \\')
|
|
1417
|
+
lines.append(f" -d '{body}'")
|
|
1418
|
+
else:
|
|
1419
|
+
lines.append(f"curl http://127.0.0.1:5000{endpoint}")
|
|
1420
|
+
lines.append("```")
|
|
1421
|
+
return lines
|
|
1422
|
+
|
|
1423
|
+
@staticmethod
|
|
1424
|
+
def _extract_test_cases(test_file: Path) -> str:
|
|
1425
|
+
"""从测试文件中提取测试函数名"""
|
|
1426
|
+
try:
|
|
1427
|
+
content = test_file.read_text(encoding="utf-8", errors="replace")
|
|
1428
|
+
except Exception:
|
|
1429
|
+
return "—"
|
|
1430
|
+
import re as _re
|
|
1431
|
+
|
|
1432
|
+
names = _re.findall(r"def (test_\w+)", content)
|
|
1433
|
+
if names:
|
|
1434
|
+
return f"{len(names)} 个用例: {', '.join(names[:5])}" + (
|
|
1435
|
+
"..." if len(names) > 5 else ""
|
|
1436
|
+
)
|
|
1437
|
+
return "—"
|
|
1438
|
+
|
|
1439
|
+
@staticmethod
|
|
1440
|
+
def _detect_missing_deps(frontend_dir: Path) -> List[str]:
|
|
1441
|
+
"""检测代码中使用但 package.json 未声明的依赖"""
|
|
1442
|
+
pkg_json = frontend_dir / "package.json"
|
|
1443
|
+
if not pkg_json.exists():
|
|
1444
|
+
return []
|
|
1445
|
+
|
|
1446
|
+
try:
|
|
1447
|
+
import json
|
|
1448
|
+
|
|
1449
|
+
data = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
1450
|
+
declared = set(data.get("dependencies", {}).keys()) | set(
|
|
1451
|
+
data.get("devDependencies", {}).keys()
|
|
1452
|
+
)
|
|
1453
|
+
except Exception:
|
|
1454
|
+
return []
|
|
1455
|
+
|
|
1456
|
+
# 常用前端库名映射
|
|
1457
|
+
import_to_pkg = {
|
|
1458
|
+
"react-router-dom": "react-router-dom",
|
|
1459
|
+
"react-router": "react-router-dom",
|
|
1460
|
+
"react-toastify": "react-toastify",
|
|
1461
|
+
"axios": "axios",
|
|
1462
|
+
"lodash": "lodash",
|
|
1463
|
+
"@testing-library/react": "@testing-library/react",
|
|
1464
|
+
"@testing-library/jest-dom": "@testing-library/jest-dom",
|
|
1465
|
+
"@testing-library/user-event": "@testing-library/user-event",
|
|
1466
|
+
"redux": "redux",
|
|
1467
|
+
"react-redux": "react-redux",
|
|
1468
|
+
"mobx": "mobx",
|
|
1469
|
+
"antd": "antd",
|
|
1470
|
+
"@ant-design/icons": "@ant-design/icons",
|
|
1471
|
+
"element-plus": "element-plus",
|
|
1472
|
+
"vue-router": "vue-router",
|
|
1473
|
+
"pinia": "pinia",
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
missing = set()
|
|
1477
|
+
for f in frontend_dir.rglob("*.tsx"):
|
|
1478
|
+
try:
|
|
1479
|
+
content = f.read_text(encoding="utf-8", errors="replace")
|
|
1480
|
+
except Exception:
|
|
1481
|
+
continue
|
|
1482
|
+
for ic, pkg in import_to_pkg.items():
|
|
1483
|
+
if pkg in declared:
|
|
1484
|
+
continue
|
|
1485
|
+
if (
|
|
1486
|
+
f"from '{ic}'" in content
|
|
1487
|
+
or f'from "{ic}"' in content
|
|
1488
|
+
or f"require('{ic}')" in content
|
|
1489
|
+
or f'require("{ic}")' in content
|
|
1490
|
+
):
|
|
1491
|
+
missing.add(pkg)
|
|
1492
|
+
return sorted(missing)
|
|
1493
|
+
|
|
1494
|
+
@staticmethod
|
|
1495
|
+
def _find_first_api(output_path: Path) -> Optional[str]:
|
|
1496
|
+
"""在 backend 目录中找到第一个 API 端点,生成 curl 示例"""
|
|
1497
|
+
contracts = OutputManager._extract_api_contracts(output_path / "backend")
|
|
1498
|
+
if contracts:
|
|
1499
|
+
c = contracts[0]
|
|
1500
|
+
method = c.get("method", "GET")
|
|
1501
|
+
endpoint = c.get("endpoint", "/")
|
|
1502
|
+
if method.upper() in ("POST", "PUT"):
|
|
1503
|
+
return (
|
|
1504
|
+
f"-X {method.upper()} http://127.0.0.1:5000{endpoint} "
|
|
1505
|
+
f"-H 'Content-Type: application/json' "
|
|
1506
|
+
f'-d \'{{"key":"value"}}\''
|
|
1507
|
+
)
|
|
1508
|
+
else:
|
|
1509
|
+
return f"http://127.0.0.1:5000{endpoint}"
|
|
1510
|
+
return None
|
|
1511
|
+
|
|
1512
|
+
# ------------------------------------------------------------------ #
|
|
1513
|
+
# 内部工具方法
|
|
1514
|
+
# ------------------------------------------------------------------ #
|
|
1515
|
+
|
|
1516
|
+
@staticmethod
|
|
1517
|
+
def _slugify(text: str) -> str:
|
|
1518
|
+
"""
|
|
1519
|
+
将文本转换为安全的目录名:
|
|
1520
|
+
- 保留中文、字母、数字
|
|
1521
|
+
- 其他字符替换为下划线
|
|
1522
|
+
- 合并多个下划线为一个
|
|
1523
|
+
"""
|
|
1524
|
+
# 保留中文、字母、数字,其余替换为下划线
|
|
1525
|
+
safe = re.sub(r"[^\w\u4e00-\u9fff]", "_", text)
|
|
1526
|
+
# 合并多个下划线
|
|
1527
|
+
safe = re.sub(r"_+", "_", safe)
|
|
1528
|
+
return safe.strip("_")
|
|
1529
|
+
|
|
1530
|
+
@staticmethod
|
|
1531
|
+
def _collect_code(directory: Path) -> str:
|
|
1532
|
+
"""
|
|
1533
|
+
收集目录下所有代码文件内容,合并为单个字符串
|
|
1534
|
+
每个文件用 ### FILE: 标记分隔
|
|
1535
|
+
"""
|
|
1536
|
+
if not directory.exists():
|
|
1537
|
+
return ""
|
|
1538
|
+
parts = []
|
|
1539
|
+
for file_path in sorted(directory.rglob("*")):
|
|
1540
|
+
if file_path.is_file():
|
|
1541
|
+
rel = file_path.relative_to(directory.parent) # 相对于 output_dir
|
|
1542
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
1543
|
+
parts.append(f"### FILE: {rel}\n{content}\n")
|
|
1544
|
+
return "\n".join(parts)
|
|
1545
|
+
|
|
1546
|
+
@staticmethod
|
|
1547
|
+
def _collect_audits(directory: Path) -> List[str]:
|
|
1548
|
+
"""收集 audits/ 目录下所有报告内容"""
|
|
1549
|
+
if not directory.exists():
|
|
1550
|
+
return []
|
|
1551
|
+
reports = []
|
|
1552
|
+
for file_path in sorted(directory.glob("*.md")):
|
|
1553
|
+
reports.append(file_path.read_text(encoding="utf-8", errors="replace"))
|
|
1554
|
+
return reports
|
|
1555
|
+
|
|
1556
|
+
@staticmethod
|
|
1557
|
+
def _format_iteration_content(prompt: str, result: Dict, meta: Dict) -> str:
|
|
1558
|
+
"""格式化迭代记录为 Markdown"""
|
|
1559
|
+
lines = [
|
|
1560
|
+
f"# 迭代记录 #{meta.get('iteration_count', 0)}",
|
|
1561
|
+
"",
|
|
1562
|
+
f"- 时间: {datetime.now().isoformat()}",
|
|
1563
|
+
f"- 提示词: {prompt}",
|
|
1564
|
+
"",
|
|
1565
|
+
"## 执行结果",
|
|
1566
|
+
"",
|
|
1567
|
+
"```json",
|
|
1568
|
+
json.dumps(result, ensure_ascii=False, indent=2),
|
|
1569
|
+
"```",
|
|
1570
|
+
"",
|
|
1571
|
+
]
|
|
1572
|
+
return "\n".join(lines)
|