AIDeveloper-PC/requirements_generator/main.py

988 lines
42 KiB
Python
Raw Normal View History

2026-03-04 18:09:45 +00:00
#!/usr/bin/env python3
2026-03-05 06:01:40 +00:00
# main.py - 主入口:支持交互式 & 非交互式,含项目管理子命令
2026-03-04 18:09:45 +00:00
import os
import sys
2026-03-05 05:38:26 +00:00
from typing import Dict, List
2026-03-04 18:09:45 +00:00
import click
from rich.console import Console
2026-03-05 06:01:40 +00:00
from rich.table import Table
from rich.panel import Panel
from rich.prompt import Prompt, Confirm
2026-03-04 18:09:45 +00:00
import config
from database.db_manager import DBManager
2026-03-05 06:01:40 +00:00
from database.models import Project, RawRequirement, FunctionalRequirement
from core.llm_client import LLMClient
2026-03-04 18:09:45 +00:00
from core.requirement_analyzer import RequirementAnalyzer
2026-03-05 06:01:40 +00:00
from core.code_generator import CodeGenerator
from utils.file_handler import read_file_auto, merge_knowledge_files
2026-03-04 18:09:45 +00:00
from utils.output_writer import (
2026-03-05 05:38:26 +00:00
ensure_project_dir, build_project_output_dir,
write_project_readme, write_function_signatures_json,
validate_all_signatures, patch_signatures_with_url,
2026-03-05 06:01:40 +00:00
render_project_info, render_project_list,
2026-03-04 18:09:45 +00:00
)
console = Console()
db = DBManager()
# ══════════════════════════════════════════════════════
# 显示工具函数
# ══════════════════════════════════════════════════════
def print_banner():
console.print(Panel.fit(
"[bold cyan]🚀 需求分析 & 代码生成工具[/bold cyan]\n"
"[dim]Powered by LLM · SQLite · Python[/dim]",
2026-03-05 05:38:26 +00:00
border_style="cyan",
2026-03-04 18:09:45 +00:00
))
2026-03-05 05:38:26 +00:00
def print_functional_requirements(reqs: List[FunctionalRequirement]):
2026-03-04 18:09:45 +00:00
table = Table(title="📋 功能需求列表", show_lines=True)
2026-03-05 05:38:26 +00:00
table.add_column("序号", style="cyan", width=6)
table.add_column("ID", style="dim", width=6)
table.add_column("模块", style="magenta", width=15)
table.add_column("标题", style="bold", width=18)
table.add_column("函数名", width=25)
table.add_column("优先级", width=8)
table.add_column("描述", width=35)
2026-03-04 18:09:45 +00:00
priority_color = {"high": "red", "medium": "yellow", "low": "green"}
for req in reqs:
color = priority_color.get(req.priority, "white")
table.add_row(
str(req.index_no),
str(req.id) if req.id else "-",
2026-03-05 05:38:26 +00:00
req.module or config.DEFAULT_MODULE,
2026-03-04 18:09:45 +00:00
req.title,
2026-03-05 06:01:40 +00:00
req.function_name,
2026-03-04 18:09:45 +00:00
f"[{color}]{req.priority}[/{color}]",
2026-03-05 05:38:26 +00:00
req.description[:50] + "..." if len(req.description) > 50 else req.description,
2026-03-04 18:09:45 +00:00
)
console.print(table)
2026-03-05 05:38:26 +00:00
def print_module_summary(reqs: List[FunctionalRequirement]):
module_map: Dict[str, List[str]] = {}
for req in reqs:
m = req.module or config.DEFAULT_MODULE
module_map.setdefault(m, []).append(req.function_name)
table = Table(title="📦 功能模块分组", show_lines=True)
table.add_column("模块", style="magenta bold", width=20)
table.add_column("函数数量", style="cyan", width=8)
table.add_column("函数列表", width=50)
for module, funcs in sorted(module_map.items()):
table.add_row(module, str(len(funcs)), ", ".join(funcs))
console.print(table)
2026-03-04 18:09:45 +00:00
2026-03-05 05:38:26 +00:00
def print_signatures_preview(signatures: List[dict]):
2026-03-04 18:09:45 +00:00
table = Table(title="📄 函数签名预览", show_lines=True)
2026-03-05 05:38:26 +00:00
table.add_column("需求编号", style="cyan", width=8)
table.add_column("模块", style="magenta", width=15)
table.add_column("函数名", style="bold", width=22)
table.add_column("参数数", width=6)
table.add_column("返回类型", width=10)
table.add_column("URL", style="dim", width=28)
2026-03-04 18:09:45 +00:00
for sig in signatures:
2026-03-05 05:38:26 +00:00
ret = sig.get("return") or {}
url = sig.get("url", "")
2026-03-04 18:09:45 +00:00
url_display = os.path.basename(url) if url else "[dim]待生成[/dim]"
table.add_row(
sig.get("requirement_id", "-"),
2026-03-05 05:38:26 +00:00
sig.get("module", "-"),
2026-03-04 18:09:45 +00:00
sig.get("name", "-"),
str(len(sig.get("parameters", {}))),
ret.get("type", "void"),
url_display,
)
console.print(table)
# ══════════════════════════════════════════════════════
# Step 1项目初始化
# ══════════════════════════════════════════════════════
def step_init_project(
2026-03-05 06:01:40 +00:00
project_name: str = None, language: str = None,
description: str = "", non_interactive: bool = False,
2026-03-04 18:09:45 +00:00
) -> Project:
2026-03-05 05:38:26 +00:00
console.print(
"\n[bold]Step 1 · 项目配置[/bold]"
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
2026-03-04 18:09:45 +00:00
if not non_interactive:
2026-03-05 05:38:26 +00:00
project_name = project_name or Prompt.ask("📁 项目名称")
2026-03-05 06:01:40 +00:00
language = language or Prompt.ask(
2026-03-05 05:38:26 +00:00
"💻 目标语言", default=config.DEFAULT_LANGUAGE,
choices=["python","javascript","typescript","java","go","rust"],
2026-03-04 18:09:45 +00:00
)
description = description or Prompt.ask("📝 项目描述(可选)", default="")
else:
if not project_name:
raise ValueError("非交互模式下 --project-name 为必填项")
language = language or config.DEFAULT_LANGUAGE
2026-03-05 05:38:26 +00:00
console.print(f" 项目: {project_name} 语言: {language}")
2026-03-04 18:09:45 +00:00
existing = db.get_project_by_name(project_name)
if existing:
if non_interactive:
2026-03-05 05:38:26 +00:00
console.print(f"[green]✓ 已加载项目: {project_name} (ID={existing.id})[/green]")
2026-03-04 18:09:45 +00:00
return existing
2026-03-05 05:38:26 +00:00
if Confirm.ask(f"⚠️ 项目 '{project_name}' 已存在,继续使用?"):
2026-03-04 18:09:45 +00:00
console.print(f"[green]✓ 已加载项目: {project_name} (ID={existing.id})[/green]")
return existing
project_name = Prompt.ask("请输入新的项目名称")
2026-03-05 06:01:40 +00:00
project = Project(
2026-03-05 05:38:26 +00:00
name = project_name,
language = language,
output_dir = build_project_output_dir(project_name),
description = description,
2026-03-04 18:09:45 +00:00
)
project.id = db.create_project(project)
console.print(f"[green]✓ 项目已创建: {project_name} (ID={project.id})[/green]")
return project
# ══════════════════════════════════════════════════════
# Step 2输入原始需求 & 知识库
# ══════════════════════════════════════════════════════
def step_input_requirement(
2026-03-05 06:01:40 +00:00
project: Project,
requirement_text: str = None,
requirement_file: str = None,
knowledge_files: list = None,
non_interactive: bool = False,
2026-03-04 18:09:45 +00:00
) -> tuple:
console.print(
2026-03-05 05:38:26 +00:00
"\n[bold]Step 2 · 输入原始需求[/bold]"
2026-03-04 18:09:45 +00:00
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
raw_text = ""
source_name = None
source_type = "text"
if non_interactive:
if requirement_file:
raw_text = read_file_auto(requirement_file)
source_name = os.path.basename(requirement_file)
source_type = "file"
console.print(f" 需求文件: {source_name} ({len(raw_text)} 字符)")
elif requirement_text:
2026-03-05 06:01:40 +00:00
raw_text = requirement_text
2026-03-05 05:38:26 +00:00
console.print(f" 需求文本: {raw_text[:80]}{'...' if len(raw_text)>80 else ''}")
2026-03-04 18:09:45 +00:00
else:
raise ValueError("非交互模式下必须提供 --requirement-text 或 --requirement-file")
else:
2026-03-05 05:38:26 +00:00
input_type = Prompt.ask("📥 需求输入方式", choices=["text","file"], default="text")
2026-03-04 18:09:45 +00:00
if input_type == "text":
console.print("[dim]请输入原始需求(输入空行结束):[/dim]")
lines = []
while True:
line = input()
if line == "" and lines:
break
lines.append(line)
2026-03-05 05:38:26 +00:00
raw_text = "\n".join(lines)
2026-03-04 18:09:45 +00:00
else:
2026-03-05 05:38:26 +00:00
fp = Prompt.ask("📂 需求文件路径")
raw_text = read_file_auto(fp)
source_name = os.path.basename(fp)
2026-03-04 18:09:45 +00:00
source_type = "file"
2026-03-05 05:38:26 +00:00
console.print(f"[green]✓ 已读取: {source_name} ({len(raw_text)} 字符)[/green]")
2026-03-04 18:09:45 +00:00
knowledge_text = ""
if non_interactive:
if knowledge_files:
knowledge_text = merge_knowledge_files(list(knowledge_files))
console.print(f" 知识库: {len(knowledge_files)} 个文件,{len(knowledge_text)} 字符")
else:
2026-03-05 05:38:26 +00:00
if Confirm.ask("📚 是否输入知识库文件?", default=False):
2026-03-04 18:09:45 +00:00
kb_paths = []
while True:
2026-03-05 05:38:26 +00:00
p = Prompt.ask("知识库文件路径(留空结束)", default="")
if not p:
2026-03-04 18:09:45 +00:00
break
2026-03-05 05:38:26 +00:00
if os.path.exists(p):
kb_paths.append(p)
console.print(f" [green]+ {p}[/green]")
2026-03-04 18:09:45 +00:00
else:
2026-03-05 05:38:26 +00:00
console.print(f" [red]文件不存在: {p}[/red]")
2026-03-04 18:09:45 +00:00
if kb_paths:
knowledge_text = merge_knowledge_files(kb_paths)
console.print(f"[green]✓ 知识库已合并 ({len(knowledge_text)} 字符)[/green]")
return raw_text, knowledge_text, source_name, source_type
# ══════════════════════════════════════════════════════
# Step 3LLM 分解需求
# ══════════════════════════════════════════════════════
def step_decompose_requirements(
2026-03-05 06:01:40 +00:00
project: Project, raw_text: str, knowledge_text: str,
source_name: str, source_type: str, non_interactive: bool = False,
2026-03-04 18:09:45 +00:00
) -> tuple:
console.print(
2026-03-05 05:38:26 +00:00
"\n[bold]Step 3 · LLM 需求分解[/bold]"
2026-03-04 18:09:45 +00:00
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
raw_req = RawRequirement(
2026-03-05 05:38:26 +00:00
project_id = project.id,
content = raw_text,
source_type = source_type,
source_name = source_name,
knowledge = knowledge_text or None,
2026-03-04 18:09:45 +00:00
)
raw_req_id = db.create_raw_requirement(raw_req)
console.print(f"[dim]原始需求已存储 (ID={raw_req_id})[/dim]")
2026-03-05 05:38:26 +00:00
with console.status("[bold yellow]🤖 LLM 正在分解需求...[/bold yellow]"):
2026-03-04 18:09:45 +00:00
llm = LLMClient()
analyzer = RequirementAnalyzer(llm)
func_reqs = analyzer.decompose(
2026-03-05 05:38:26 +00:00
raw_requirement = raw_text,
project_id = project.id,
raw_req_id = raw_req_id,
knowledge = knowledge_text,
2026-03-04 18:09:45 +00:00
)
for req in func_reqs:
req.id = db.create_functional_requirement(req)
console.print(f"[green]✓ 已生成 {len(func_reqs)} 个功能需求[/green]")
return raw_req_id, func_reqs
# ══════════════════════════════════════════════════════
2026-03-05 06:01:40 +00:00
# Step 4模块分类
2026-03-05 05:38:26 +00:00
# ══════════════════════════════════════════════════════
def step_classify_modules(
2026-03-05 06:01:40 +00:00
project: Project, func_reqs: List[FunctionalRequirement],
knowledge_text: str = "", non_interactive: bool = False,
2026-03-05 05:38:26 +00:00
) -> List[FunctionalRequirement]:
console.print(
"\n[bold]Step 4 · 功能模块分类[/bold]"
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
with console.status("[bold yellow]🤖 LLM 正在进行模块分类...[/bold yellow]"):
llm = LLMClient()
analyzer = RequirementAnalyzer(llm)
try:
2026-03-05 06:01:40 +00:00
updates = analyzer.classify_modules(func_reqs, knowledge_text)
2026-03-05 05:38:26 +00:00
name_to_module = {u["function_name"]: u["module"] for u in updates}
for req in func_reqs:
req.module = name_to_module.get(req.function_name, config.DEFAULT_MODULE)
db.update_functional_requirement(req)
2026-03-05 06:01:40 +00:00
console.print("[green]✓ LLM 模块分类完成[/green]")
2026-03-05 05:38:26 +00:00
except Exception as e:
console.print(f"[yellow]⚠ 模块分类失败,保留原有模块: {e}[/yellow]")
print_module_summary(func_reqs)
if non_interactive:
return func_reqs
while True:
console.print(
"\n模块操作: [cyan]r[/cyan]=重新分类 "
2026-03-05 06:01:40 +00:00
"[cyan]e[/cyan]=手动编辑 [cyan]ok[/cyan]=确认继续"
2026-03-05 05:38:26 +00:00
)
action = Prompt.ask("请选择操作", default="ok").strip().lower()
if action == "ok":
break
elif action == "r":
with console.status("[bold yellow]🤖 重新分类中...[/bold yellow]"):
try:
2026-03-05 06:01:40 +00:00
updates = analyzer.classify_modules(func_reqs, knowledge_text)
2026-03-05 05:38:26 +00:00
name_to_module = {u["function_name"]: u["module"] for u in updates}
for req in func_reqs:
req.module = name_to_module.get(req.function_name, config.DEFAULT_MODULE)
db.update_functional_requirement(req)
console.print("[green]✓ 重新分类完成[/green]")
except Exception as e:
console.print(f"[red]重新分类失败: {e}[/red]")
print_module_summary(func_reqs)
elif action == "e":
print_functional_requirements(func_reqs)
idx_str = Prompt.ask("输入要修改模块的需求序号")
if not idx_str.isdigit():
continue
2026-03-05 06:01:40 +00:00
target = next((r for r in func_reqs if r.index_no == int(idx_str)), None)
2026-03-05 05:38:26 +00:00
if target is None:
console.print("[red]序号不存在[/red]")
continue
2026-03-05 06:01:40 +00:00
new_module = Prompt.ask(
2026-03-05 05:38:26 +00:00
f"新模块名(当前: {target.module}",
default=target.module or config.DEFAULT_MODULE,
)
2026-03-05 06:01:40 +00:00
target.module = new_module.strip() or config.DEFAULT_MODULE
2026-03-05 05:38:26 +00:00
db.update_functional_requirement(target)
2026-03-05 06:01:40 +00:00
console.print(f"[green]✓ '{target.function_name}'{target.module}[/green]")
2026-03-05 05:38:26 +00:00
print_module_summary(func_reqs)
return func_reqs
# ══════════════════════════════════════════════════════
# Step 5编辑功能需求
2026-03-04 18:09:45 +00:00
# ══════════════════════════════════════════════════════
def step_edit_requirements(
2026-03-05 06:01:40 +00:00
project: Project, func_reqs: List[FunctionalRequirement],
raw_req_id: int, non_interactive: bool = False, skip_indices: list = None,
2026-03-05 05:38:26 +00:00
) -> List[FunctionalRequirement]:
2026-03-04 18:09:45 +00:00
console.print(
2026-03-05 05:38:26 +00:00
"\n[bold]Step 5 · 编辑功能需求[/bold]"
2026-03-04 18:09:45 +00:00
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
if non_interactive:
if skip_indices:
to_skip = set(skip_indices)
removed, kept = [], []
for req in func_reqs:
if req.index_no in to_skip:
db.delete_functional_requirement(req.id)
removed.append(req.title)
else:
kept.append(req)
func_reqs = kept
for i, req in enumerate(func_reqs, 1):
req.index_no = i
db.update_functional_requirement(req)
if removed:
console.print(f" [red]已跳过: {', '.join(removed)}[/red]")
print_functional_requirements(func_reqs)
return func_reqs
while True:
print_functional_requirements(func_reqs)
console.print(
"\n操作: [cyan]d[/cyan]=删除 [cyan]a[/cyan]=添加 "
"[cyan]e[/cyan]=编辑 [cyan]ok[/cyan]=确认继续"
)
action = Prompt.ask("请选择操作", default="ok").strip().lower()
if action == "ok":
break
elif action == "d":
2026-03-05 05:38:26 +00:00
idx_str = Prompt.ask("输入要删除的序号(多个用逗号分隔)")
2026-03-04 18:09:45 +00:00
to_delete = {int(x.strip()) for x in idx_str.split(",") if x.strip().isdigit()}
removed, kept = [], []
for req in func_reqs:
if req.index_no in to_delete:
db.delete_functional_requirement(req.id)
removed.append(req.title)
else:
kept.append(req)
func_reqs = kept
for i, req in enumerate(func_reqs, 1):
req.index_no = i
db.update_functional_requirement(req)
console.print(f"[red]✗ 已删除: {', '.join(removed)}[/red]")
elif action == "a":
2026-03-05 06:01:40 +00:00
title = Prompt.ask("功能标题")
description = Prompt.ask("功能描述")
func_name = Prompt.ask("函数名 (snake_case)")
priority = Prompt.ask("优先级", choices=["high","medium","low"], default="medium")
module = Prompt.ask("所属模块", default=config.DEFAULT_MODULE)
2026-03-04 18:09:45 +00:00
new_req = FunctionalRequirement(
2026-03-05 05:38:26 +00:00
project_id = project.id,
raw_req_id = raw_req_id,
index_no = len(func_reqs) + 1,
title = title,
description = description,
function_name = func_name,
priority = priority,
module = module.strip() or config.DEFAULT_MODULE,
is_custom = True,
2026-03-04 18:09:45 +00:00
)
new_req.id = db.create_functional_requirement(new_req)
func_reqs.append(new_req)
2026-03-05 06:01:40 +00:00
console.print(f"[green]✓ 已添加: {title}[/green]")
2026-03-04 18:09:45 +00:00
elif action == "e":
2026-03-05 05:38:26 +00:00
idx_str = Prompt.ask("输入要编辑的序号")
2026-03-04 18:09:45 +00:00
if not idx_str.isdigit():
continue
2026-03-05 06:01:40 +00:00
target = next((r for r in func_reqs if r.index_no == int(idx_str)), None)
2026-03-04 18:09:45 +00:00
if target is None:
console.print("[red]序号不存在[/red]")
continue
target.title = Prompt.ask("新标题", default=target.title)
target.description = Prompt.ask("新描述", default=target.description)
target.function_name = Prompt.ask("新函数名", default=target.function_name)
target.priority = Prompt.ask(
2026-03-05 05:38:26 +00:00
"新优先级", choices=["high","medium","low"], default=target.priority
2026-03-04 18:09:45 +00:00
)
2026-03-05 06:01:40 +00:00
target.module = Prompt.ask(
2026-03-05 05:38:26 +00:00
"新模块", default=target.module or config.DEFAULT_MODULE
).strip() or config.DEFAULT_MODULE
2026-03-04 18:09:45 +00:00
db.update_functional_requirement(target)
console.print(f"[green]✓ 已更新: {target.title}[/green]")
return func_reqs
# ══════════════════════════════════════════════════════
2026-03-05 06:01:40 +00:00
# Step 6A生成函数签名
2026-03-04 18:09:45 +00:00
# ══════════════════════════════════════════════════════
def step_generate_signatures(
2026-03-05 06:01:40 +00:00
project: Project, func_reqs: List[FunctionalRequirement],
output_dir: str, knowledge_text: str,
json_file_name: str = "function_signatures.json",
2026-03-04 18:09:45 +00:00
non_interactive: bool = False,
) -> tuple:
console.print(
2026-03-05 05:38:26 +00:00
"\n[bold]Step 6A · 生成函数签名 JSON[/bold]"
2026-03-04 18:09:45 +00:00
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
2026-03-05 06:01:40 +00:00
llm = LLMClient()
analyzer = RequirementAnalyzer(llm)
2026-03-04 18:09:45 +00:00
success_count = 0
fail_count = 0
def on_progress(index, total, req, signature, error):
nonlocal success_count, fail_count
if error:
console.print(
2026-03-05 06:01:40 +00:00
f" [{index}/{total}] [yellow]⚠ {req.title} 签名生成失败(降级): {error}[/yellow]"
2026-03-04 18:09:45 +00:00
)
fail_count += 1
else:
console.print(
f" [{index}/{total}] [green]✓ {req.title}[/green] "
2026-03-05 05:38:26 +00:00
f"[dim]{req.module}[/dim] → {signature.get('name')}()"
2026-03-04 18:09:45 +00:00
)
success_count += 1
console.print(f"[yellow]正在为 {len(func_reqs)} 个功能需求生成函数签名...[/yellow]")
signatures = analyzer.build_function_signatures_batch(
2026-03-05 06:01:40 +00:00
func_reqs=func_reqs, knowledge=knowledge_text, on_progress=on_progress,
2026-03-04 18:09:45 +00:00
)
2026-03-05 05:38:26 +00:00
report = validate_all_signatures(signatures)
if report:
2026-03-05 06:01:40 +00:00
console.print(f"[yellow]⚠ {len(report)} 个签名存在结构问题[/yellow]")
2026-03-05 05:38:26 +00:00
for fname, errs in report.items():
for err in errs:
2026-03-04 18:09:45 +00:00
console.print(f" [yellow]· {fname}: {err}[/yellow]")
else:
console.print("[green]✓ 所有签名结构校验通过[/green]")
json_path = write_function_signatures_json(
2026-03-05 06:01:40 +00:00
output_dir=output_dir, signatures=signatures,
project_name=project.name, project_description=project.description or "",
file_name=json_file_name,
2026-03-04 18:09:45 +00:00
)
console.print(
f"[green]✓ 签名 JSON 初版已写入: [cyan]{os.path.abspath(json_path)}[/cyan][/green]\n"
f" 成功: {success_count} 降级: {fail_count}"
)
return signatures, json_path
# ══════════════════════════════════════════════════════
2026-03-05 06:01:40 +00:00
# Step 6B生成代码文件
2026-03-04 18:09:45 +00:00
# ══════════════════════════════════════════════════════
def step_generate_code(
2026-03-05 06:01:40 +00:00
project: Project, func_reqs: List[FunctionalRequirement],
output_dir: str, knowledge_text: str, signatures: List[dict],
2026-03-04 18:09:45 +00:00
non_interactive: bool = False,
) -> Dict[str, str]:
console.print(
2026-03-05 05:38:26 +00:00
"\n[bold]Step 6B · 生成代码文件[/bold]"
2026-03-04 18:09:45 +00:00
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
2026-03-05 05:38:26 +00:00
generator = CodeGenerator(LLMClient())
success_count = 0
fail_count = 0
func_name_to_url: Dict[str, str] = {}
2026-03-04 18:09:45 +00:00
def on_progress(index, total, req, code_file, error):
nonlocal success_count, fail_count
if error:
console.print(f" [{index}/{total}] [red]✗ {req.title}: {error}[/red]")
fail_count += 1
else:
db.upsert_code_file(code_file)
req.status = "generated"
db.update_functional_requirement(req)
func_name_to_url[req.function_name] = os.path.abspath(code_file.file_path)
console.print(
f" [{index}/{total}] [green]✓ {req.title}[/green] "
2026-03-05 05:38:26 +00:00
f"[dim]{req.module}/{code_file.file_name}[/dim]"
2026-03-04 18:09:45 +00:00
)
success_count += 1
2026-03-05 06:01:40 +00:00
console.print(f"[yellow]开始生成 {len(func_reqs)} 个代码文件...[/yellow]")
2026-03-04 18:09:45 +00:00
generator.generate_batch(
2026-03-05 06:01:40 +00:00
func_reqs=func_reqs, output_dir=output_dir,
language=project.language, knowledge=knowledge_text,
signatures=signatures, on_progress=on_progress,
2026-03-04 18:09:45 +00:00
)
2026-03-05 06:01:40 +00:00
modules = list({req.module or config.DEFAULT_MODULE for req in func_reqs})
2026-03-04 18:09:45 +00:00
req_summary = "\n".join(
2026-03-05 05:38:26 +00:00
f"{i+1}. **{r.title}** (`{r.module}/{r.function_name}`) - {r.description[:80]}"
2026-03-04 18:09:45 +00:00
for i, r in enumerate(func_reqs)
)
2026-03-05 05:38:26 +00:00
write_project_readme(
2026-03-05 06:01:40 +00:00
output_dir=output_dir, project_name=project.name,
project_description=project.description or "",
requirements_summary=req_summary, modules=modules,
2026-03-05 05:38:26 +00:00
)
2026-03-04 18:09:45 +00:00
console.print(Panel(
f"[bold green]✅ 代码生成完成![/bold green]\n"
f"成功: {success_count} 失败: {fail_count}\n"
f"输出目录: [cyan]{os.path.abspath(output_dir)}[/cyan]",
border_style="green",
))
return func_name_to_url
# ══════════════════════════════════════════════════════
2026-03-05 06:01:40 +00:00
# Step 6C回写 url 字段
2026-03-04 18:09:45 +00:00
# ══════════════════════════════════════════════════════
def step_patch_signatures_url(
2026-03-05 06:01:40 +00:00
project: Project, signatures: List[dict],
func_name_to_url: Dict[str, str], output_dir: str,
json_file_name: str, non_interactive: bool = False,
2026-03-04 18:09:45 +00:00
) -> str:
console.print(
2026-03-05 05:38:26 +00:00
"\n[bold]Step 6C · 回写代码路径url到签名 JSON[/bold]"
2026-03-04 18:09:45 +00:00
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
patch_signatures_with_url(signatures, func_name_to_url)
patched = sum(1 for s in signatures if s.get("url"))
unpatched = len(signatures) - patched
if unpatched:
2026-03-05 06:01:40 +00:00
console.print(f"[yellow]⚠ {unpatched} 个函数 url 未回写[/yellow]")
2026-03-04 18:09:45 +00:00
print_signatures_preview(signatures)
json_path = write_function_signatures_json(
2026-03-05 06:01:40 +00:00
output_dir=output_dir, signatures=signatures,
project_name=project.name, project_description=project.description or "",
file_name=json_file_name,
2026-03-04 18:09:45 +00:00
)
console.print(
f"[green]✓ 签名 JSON 已更新(含 url: "
f"[cyan]{os.path.abspath(json_path)}[/cyan][/green]\n"
f" 已回写: {patched} 未回写: {unpatched}"
)
return os.path.abspath(json_path)
# ══════════════════════════════════════════════════════
# 核心工作流
# ══════════════════════════════════════════════════════
def run_workflow(
2026-03-05 06:01:40 +00:00
project_name: str = None, language: str = None, description: str = "",
requirement_text: str = None, requirement_file: str = None,
knowledge_files: tuple = (), skip_indices: list = None,
json_file_name: str = "function_signatures.json",
non_interactive: bool = False,
2026-03-04 18:09:45 +00:00
):
2026-03-05 06:01:40 +00:00
"""Step 1 → 6C 完整工作流"""
2026-03-04 18:09:45 +00:00
print_banner()
2026-03-05 06:01:40 +00:00
# Step 1
2026-03-04 18:09:45 +00:00
project = step_init_project(
2026-03-05 06:01:40 +00:00
project_name=project_name, language=language,
description=description, non_interactive=non_interactive,
2026-03-04 18:09:45 +00:00
)
2026-03-05 06:01:40 +00:00
# Step 2
2026-03-04 18:09:45 +00:00
raw_text, knowledge_text, source_name, source_type = step_input_requirement(
2026-03-05 06:01:40 +00:00
project=project, requirement_text=requirement_text,
requirement_file=requirement_file,
knowledge_files=list(knowledge_files) if knowledge_files else [],
non_interactive=non_interactive,
2026-03-04 18:09:45 +00:00
)
2026-03-05 06:01:40 +00:00
# Step 3
2026-03-04 18:09:45 +00:00
raw_req_id, func_reqs = step_decompose_requirements(
2026-03-05 06:01:40 +00:00
project=project, raw_text=raw_text, knowledge_text=knowledge_text,
source_name=source_name, source_type=source_type,
non_interactive=non_interactive,
2026-03-04 18:09:45 +00:00
)
2026-03-05 06:01:40 +00:00
# Step 4
2026-03-05 05:38:26 +00:00
func_reqs = step_classify_modules(
2026-03-05 06:01:40 +00:00
project=project, func_reqs=func_reqs,
knowledge_text=knowledge_text, non_interactive=non_interactive,
2026-03-05 05:38:26 +00:00
)
2026-03-05 06:01:40 +00:00
# Step 5
2026-03-04 18:09:45 +00:00
func_reqs = step_edit_requirements(
2026-03-05 06:01:40 +00:00
project=project, func_reqs=func_reqs, raw_req_id=raw_req_id,
non_interactive=non_interactive, skip_indices=skip_indices or [],
2026-03-04 18:09:45 +00:00
)
if not func_reqs:
console.print("[red]⚠ 功能需求列表为空,流程终止[/red]")
return
output_dir = ensure_project_dir(project.name)
2026-03-05 06:01:40 +00:00
# Step 6A
2026-03-04 18:09:45 +00:00
signatures, json_path = step_generate_signatures(
2026-03-05 06:01:40 +00:00
project=project, func_reqs=func_reqs, output_dir=output_dir,
knowledge_text=knowledge_text, json_file_name=json_file_name,
non_interactive=non_interactive,
2026-03-04 18:09:45 +00:00
)
2026-03-05 06:01:40 +00:00
# Step 6B
2026-03-04 18:09:45 +00:00
func_name_to_url = step_generate_code(
2026-03-05 06:01:40 +00:00
project=project, func_reqs=func_reqs, output_dir=output_dir,
knowledge_text=knowledge_text, signatures=signatures,
non_interactive=non_interactive,
2026-03-04 18:09:45 +00:00
)
2026-03-05 06:01:40 +00:00
# Step 6C
2026-03-04 18:09:45 +00:00
json_path = step_patch_signatures_url(
2026-03-05 06:01:40 +00:00
project=project, signatures=signatures,
func_name_to_url=func_name_to_url, output_dir=output_dir,
json_file_name=json_file_name, non_interactive=non_interactive,
2026-03-04 18:09:45 +00:00
)
2026-03-05 05:38:26 +00:00
modules = sorted({req.module or config.DEFAULT_MODULE for req in func_reqs})
2026-03-04 18:09:45 +00:00
console.print(Panel(
f"[bold cyan]🎉 全部流程完成![/bold cyan]\n"
f"项目: [bold]{project.name}[/bold]\n"
f"描述: {project.description or '(无)'}\n"
2026-03-05 05:38:26 +00:00
f"模块: {', '.join(modules)}\n"
2026-03-04 18:09:45 +00:00
f"代码目录: [cyan]{os.path.abspath(output_dir)}[/cyan]\n"
f"签名文件: [cyan]{json_path}[/cyan]",
border_style="cyan",
))
# ══════════════════════════════════════════════════════
2026-03-05 06:01:40 +00:00
# CLI 根命令组
2026-03-04 18:09:45 +00:00
# ══════════════════════════════════════════════════════
2026-03-05 06:01:40 +00:00
@click.group()
def cli():
"""
需求分析 & 代码生成工具
\b
子命令
run 运行完整工作流需求分解 代码生成
project-list 查看所有项目列表
project-info 查看指定项目详情需求-模块-代码关系
project-delete 删除指定项目
"""
pass
# ══════════════════════════════════════════════════════
# 子命令run原工作流
# ══════════════════════════════════════════════════════
@cli.command("run")
@click.option("--non-interactive", is_flag=True, default=False,
2026-03-05 05:38:26 +00:00
help="以非交互模式运行")
2026-03-05 06:01:40 +00:00
@click.option("--project-name", "-p", default=None,
help="项目名称")
@click.option("--language", "-l", default=None,
2026-03-04 18:09:45 +00:00
type=click.Choice(["python","javascript","typescript","java","go","rust"]),
help=f"目标代码语言(默认: {config.DEFAULT_LANGUAGE}")
2026-03-05 06:01:40 +00:00
@click.option("--description", "-d", default="",
help="项目描述")
@click.option("--requirement-text", "-r", default=None,
help="原始需求文本")
@click.option("--requirement-file", "-f", default=None,
type=click.Path(exists=True),
help="原始需求文件路径")
@click.option("--knowledge-file", "-k", default=None, multiple=True,
type=click.Path(exists=True),
help="知识库文件(可多次指定)")
@click.option("--skip-index", "-s", default=None, multiple=True, type=int,
2026-03-05 05:38:26 +00:00
help="跳过的功能需求序号(可多次指定)")
2026-03-05 06:01:40 +00:00
@click.option("--json-file-name","-j", default="function_signatures.json",
2026-03-05 05:38:26 +00:00
help="签名 JSON 文件名")
2026-03-05 06:01:40 +00:00
def cmd_run(
2026-03-04 18:09:45 +00:00
non_interactive, project_name, language, description,
requirement_text, requirement_file, knowledge_file,
skip_index, json_file_name,
):
"""
2026-03-05 06:01:40 +00:00
运行完整工作流需求分解 模块分类 代码生成
2026-03-04 18:09:45 +00:00
\b
2026-03-05 05:38:26 +00:00
交互式运行
2026-03-05 06:01:40 +00:00
python main.py run
2026-03-04 18:09:45 +00:00
\b
2026-03-05 06:01:40 +00:00
非交互式示例
python main.py run --non-interactive \\
2026-03-04 18:09:45 +00:00
--project-name "UserSystem" \\
--language python \\
2026-03-05 06:01:40 +00:00
--description "用户管理系统后端" \\
--requirement-text "包含注册、登录、修改密码功能"
2026-03-04 18:09:45 +00:00
"""
try:
run_workflow(
2026-03-05 05:38:26 +00:00
project_name = project_name,
language = language,
description = description,
requirement_text = requirement_text,
requirement_file = requirement_file,
knowledge_files = knowledge_file,
skip_indices = list(skip_index) if skip_index else [],
json_file_name = json_file_name,
non_interactive = non_interactive,
2026-03-04 18:09:45 +00:00
)
except KeyboardInterrupt:
console.print("\n[yellow]用户中断,退出[/yellow]")
sys.exit(0)
except Exception as e:
console.print(f"\n[bold red]❌ 错误: {e}[/bold red]")
2026-03-05 06:01:40 +00:00
import traceback; traceback.print_exc()
2026-03-04 18:09:45 +00:00
sys.exit(1)
2026-03-05 06:01:40 +00:00
# ══════════════════════════════════════════════════════
# 子命令project-list
# ══════════════════════════════════════════════════════
@cli.command("project-list")
@click.option("--verbose", "-v", is_flag=True, default=False,
help="同时显示每个项目的输出目录路径")
def cmd_project_list(verbose: bool):
"""
查看所有已创建的项目列表含统计信息
\b
示例
python main.py project-list
python main.py project-list --verbose
"""
print_banner()
projects = db.list_projects()
if not projects:
console.print(Panel(
"[dim]暂无项目。\n请先运行 [cyan]python main.py run[/cyan] 创建项目。[/dim]",
border_style="dim",
))
return
# 批量获取统计信息
stats_map = {p.id: db.get_project_stats(p.id) for p in projects}
render_project_list(projects, stats_map, console)
if verbose:
console.print("\n[bold]输出目录详情:[/bold]")
for p in projects:
exists = p.output_dir and os.path.isdir(p.output_dir)
icon = "🟢" if exists else "🔴"
status = "[green]存在[/green]" if exists else "[red]不存在[/red]"
console.print(
f" {icon} [cyan]ID={p.id}[/cyan] [bold]{p.name}[/bold] "
f"{status} [dim]{p.output_dir or '(未设置)'}[/dim]"
)
console.print(
f"\n[dim]共 {len(projects)} 个项目。"
f"使用 [cyan]python main.py project-info --id <ID>[/cyan] 查看详情。[/dim]"
)
# ══════════════════════════════════════════════════════
# 子命令project-info
# ══════════════════════════════════════════════════════
@cli.command("project-info")
@click.option("--id", "-i", "project_id", default=None, type=int,
help="项目 ID与 --name 二选一)")
@click.option("--name", "-n", "project_name", default=None, type=str,
help="项目名称(与 --id 二选一)")
@click.option("--show-code", "-c", is_flag=True, default=False,
help="同时打印每个代码文件的前 30 行内容")
def cmd_project_info(project_id: int, project_name: str, show_code: bool):
"""
查看指定项目的详细信息需求-模块-代码关系树
\b
示例
python main.py project-info --id 1
python main.py project-info --name UserSystem
python main.py project-info --id 1 --show-code
"""
print_banner()
# 解析项目
project = None
if project_id:
project = db.get_project_by_id(project_id)
if project is None:
console.print(f"[red]❌ 未找到 ID={project_id} 的项目[/red]")
sys.exit(1)
elif project_name:
project = db.get_project_by_name(project_name)
if project is None:
console.print(f"[red]❌ 未找到名称为 '{project_name}' 的项目[/red]")
sys.exit(1)
else:
# 交互式选择
projects = db.list_projects()
if not projects:
console.print("[dim]暂无项目[/dim]")
return
stats_map = {p.id: db.get_project_stats(p.id) for p in projects}
render_project_list(projects, stats_map, console)
id_str = Prompt.ask("\n请输入要查看的项目 ID")
if not id_str.isdigit():
console.print("[red]无效 ID[/red]")
sys.exit(1)
project = db.get_project_by_id(int(id_str))
if project is None:
console.print(f"[red]❌ 未找到 ID={id_str} 的项目[/red]")
sys.exit(1)
# 获取完整信息并渲染
full_info = db.get_project_full_info(project.id)
if full_info is None:
console.print(f"[red]❌ 无法获取项目信息[/red]")
sys.exit(1)
render_project_info(full_info, console)
# 可选:打印代码文件内容预览
if show_code:
console.print("\n[bold]📄 代码文件内容预览(前 30 行)[/bold]")
for module_name, mod_data in sorted(full_info["modules"].items()):
for item in mod_data["requirements"]:
for cf in item["code_files"]:
console.print(
f"\n[magenta]── {module_name}/{cf.file_name} ──[/magenta]"
)
if cf.content:
lines = cf.content.splitlines()[:30]
console.print("\n".join(lines))
if len(cf.content.splitlines()) > 30:
console.print("[dim]... (已截断)[/dim]")
elif os.path.exists(cf.file_path):
with open(cf.file_path, encoding="utf-8", errors="replace") as f:
lines = [f.readline() for _ in range(30)]
console.print("".join(lines))
else:
console.print("[red]文件不存在[/red]")
# ══════════════════════════════════════════════════════
# 子命令project-delete
# ══════════════════════════════════════════════════════
@cli.command("project-delete")
@click.option("--id", "-i", "project_id", default=None, type=int,
help="项目 ID与 --name 二选一)")
@click.option("--name", "-n", "project_name", default=None, type=str,
help="项目名称(与 --id 二选一)")
@click.option("--delete-output", "-D", is_flag=True, default=False,
help="同时删除磁盘上的输出目录(不可恢复!)")
@click.option("--yes", "-y", is_flag=True, default=False,
help="跳过确认提示,直接删除")
def cmd_project_delete(project_id: int, project_name: str,
delete_output: bool, yes: bool):
"""
删除指定项目及其所有关联数据需求代码文件记录等
\b
示例
python main.py project-delete --id 1
python main.py project-delete --name UserSystem --delete-output --yes
"""
print_banner()
# 解析项目
project = None
if project_id:
project = db.get_project_by_id(project_id)
if project is None:
console.print(f"[red]❌ 未找到 ID={project_id} 的项目[/red]")
sys.exit(1)
elif project_name:
project = db.get_project_by_name(project_name)
if project is None:
console.print(f"[red]❌ 未找到名称为 '{project_name}' 的项目[/red]")
sys.exit(1)
else:
# 交互式选择
projects = db.list_projects()
if not projects:
console.print("[dim]暂无项目[/dim]")
return
stats_map = {p.id: db.get_project_stats(p.id) for p in projects}
render_project_list(projects, stats_map, console)
id_str = Prompt.ask("\n请输入要删除的项目 ID")
if not id_str.isdigit():
console.print("[red]无效 ID[/red]")
sys.exit(1)
project = db.get_project_by_id(int(id_str))
if project is None:
console.print(f"[red]❌ 未找到 ID={id_str} 的项目[/red]")
sys.exit(1)
# 显示待删除项目信息
stats = db.get_project_stats(project.id)
console.print(Panel(
f"[bold red]⚠️ 即将删除以下项目:[/bold red]\n\n"
f" ID: [cyan]{project.id}[/cyan]\n"
f" 名称: [bold]{project.name}[/bold]\n"
f" 语言: {project.language}\n"
f" 功能需求: {stats['func_req_count']}\n"
f" 代码文件: {stats['code_file_count']}\n"
f" 输出目录: [dim]{project.output_dir or '(无)'}[/dim]\n\n"
+ (
"[bold red]⚠️ --delete-output 已启用,输出目录将被永久删除![/bold red]"
if delete_output else
"[dim]输出目录将保留在磁盘上(仅删除数据库记录)[/dim]"
),
border_style="red",
))
# 确认
if not yes:
confirmed = Confirm.ask(
f"[bold red]确认删除项目 '{project.name}'?此操作不可撤销![/bold red]",
default=False,
)
if not confirmed:
console.print("[yellow]已取消删除[/yellow]")
return
# 执行删除
success = db.delete_project(project.id, delete_output=delete_output)
if success:
msg = f"[bold green]✅ 项目 '{project.name}' (ID={project.id}) 已成功删除[/bold green]"
if delete_output and project.output_dir:
msg += f"\n[dim]输出目录已删除: {project.output_dir}[/dim]"
console.print(Panel(msg, border_style="green"))
else:
console.print(f"[red]❌ 删除失败,项目可能已不存在[/red]")
sys.exit(1)
# ══════════════════════════════════════════════════════
# 入口
# ══════════════════════════════════════════════════════
2026-03-04 18:09:45 +00:00
if __name__ == "__main__":
cli()