AIDeveloper-PC/requirements_generator/main.py

988 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# main.py - 主入口:支持交互式 & 非交互式,含项目管理子命令
import os
import sys
from typing import Dict, List
import click
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.prompt import Prompt, Confirm
import config
from database.db_manager import DBManager
from database.models import Project, RawRequirement, FunctionalRequirement
from core.llm_client import LLMClient
from core.requirement_analyzer import RequirementAnalyzer
from core.code_generator import CodeGenerator
from utils.file_handler import read_file_auto, merge_knowledge_files
from utils.output_writer import (
ensure_project_dir, build_project_output_dir,
write_project_readme, write_function_signatures_json,
validate_all_signatures, patch_signatures_with_url,
render_project_info, render_project_list,
)
console = Console()
db = DBManager()
# ══════════════════════════════════════════════════════
# 显示工具函数
# ══════════════════════════════════════════════════════
def print_banner():
console.print(Panel.fit(
"[bold cyan]🚀 需求分析 & 代码生成工具[/bold cyan]\n"
"[dim]Powered by LLM · SQLite · Python[/dim]",
border_style="cyan",
))
def print_functional_requirements(reqs: List[FunctionalRequirement]):
table = Table(title="📋 功能需求列表", show_lines=True)
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)
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 "-",
req.module or config.DEFAULT_MODULE,
req.title,
req.function_name,
f"[{color}]{req.priority}[/{color}]",
req.description[:50] + "..." if len(req.description) > 50 else req.description,
)
console.print(table)
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)
def print_signatures_preview(signatures: List[dict]):
table = Table(title="📄 函数签名预览", show_lines=True)
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)
for sig in signatures:
ret = sig.get("return") or {}
url = sig.get("url", "")
url_display = os.path.basename(url) if url else "[dim]待生成[/dim]"
table.add_row(
sig.get("requirement_id", "-"),
sig.get("module", "-"),
sig.get("name", "-"),
str(len(sig.get("parameters", {}))),
ret.get("type", "void"),
url_display,
)
console.print(table)
# ══════════════════════════════════════════════════════
# Step 1项目初始化
# ══════════════════════════════════════════════════════
def step_init_project(
project_name: str = None, language: str = None,
description: str = "", non_interactive: bool = False,
) -> Project:
console.print(
"\n[bold]Step 1 · 项目配置[/bold]"
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
if not non_interactive:
project_name = project_name or Prompt.ask("📁 项目名称")
language = language or Prompt.ask(
"💻 目标语言", default=config.DEFAULT_LANGUAGE,
choices=["python","javascript","typescript","java","go","rust"],
)
description = description or Prompt.ask("📝 项目描述(可选)", default="")
else:
if not project_name:
raise ValueError("非交互模式下 --project-name 为必填项")
language = language or config.DEFAULT_LANGUAGE
console.print(f" 项目: {project_name} 语言: {language}")
existing = db.get_project_by_name(project_name)
if existing:
if non_interactive:
console.print(f"[green]✓ 已加载项目: {project_name} (ID={existing.id})[/green]")
return existing
if Confirm.ask(f"⚠️ 项目 '{project_name}' 已存在,继续使用?"):
console.print(f"[green]✓ 已加载项目: {project_name} (ID={existing.id})[/green]")
return existing
project_name = Prompt.ask("请输入新的项目名称")
project = Project(
name = project_name,
language = language,
output_dir = build_project_output_dir(project_name),
description = description,
)
project.id = db.create_project(project)
console.print(f"[green]✓ 项目已创建: {project_name} (ID={project.id})[/green]")
return project
# ══════════════════════════════════════════════════════
# Step 2输入原始需求 & 知识库
# ══════════════════════════════════════════════════════
def step_input_requirement(
project: Project,
requirement_text: str = None,
requirement_file: str = None,
knowledge_files: list = None,
non_interactive: bool = False,
) -> tuple:
console.print(
"\n[bold]Step 2 · 输入原始需求[/bold]"
+ (" [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:
raw_text = requirement_text
console.print(f" 需求文本: {raw_text[:80]}{'...' if len(raw_text)>80 else ''}")
else:
raise ValueError("非交互模式下必须提供 --requirement-text 或 --requirement-file")
else:
input_type = Prompt.ask("📥 需求输入方式", choices=["text","file"], default="text")
if input_type == "text":
console.print("[dim]请输入原始需求(输入空行结束):[/dim]")
lines = []
while True:
line = input()
if line == "" and lines:
break
lines.append(line)
raw_text = "\n".join(lines)
else:
fp = Prompt.ask("📂 需求文件路径")
raw_text = read_file_auto(fp)
source_name = os.path.basename(fp)
source_type = "file"
console.print(f"[green]✓ 已读取: {source_name} ({len(raw_text)} 字符)[/green]")
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:
if Confirm.ask("📚 是否输入知识库文件?", default=False):
kb_paths = []
while True:
p = Prompt.ask("知识库文件路径(留空结束)", default="")
if not p:
break
if os.path.exists(p):
kb_paths.append(p)
console.print(f" [green]+ {p}[/green]")
else:
console.print(f" [red]文件不存在: {p}[/red]")
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(
project: Project, raw_text: str, knowledge_text: str,
source_name: str, source_type: str, non_interactive: bool = False,
) -> tuple:
console.print(
"\n[bold]Step 3 · LLM 需求分解[/bold]"
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
raw_req = RawRequirement(
project_id = project.id,
content = raw_text,
source_type = source_type,
source_name = source_name,
knowledge = knowledge_text or None,
)
raw_req_id = db.create_raw_requirement(raw_req)
console.print(f"[dim]原始需求已存储 (ID={raw_req_id})[/dim]")
with console.status("[bold yellow]🤖 LLM 正在分解需求...[/bold yellow]"):
llm = LLMClient()
analyzer = RequirementAnalyzer(llm)
func_reqs = analyzer.decompose(
raw_requirement = raw_text,
project_id = project.id,
raw_req_id = raw_req_id,
knowledge = knowledge_text,
)
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
# ══════════════════════════════════════════════════════
# Step 4模块分类
# ══════════════════════════════════════════════════════
def step_classify_modules(
project: Project, func_reqs: List[FunctionalRequirement],
knowledge_text: str = "", non_interactive: bool = False,
) -> 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:
updates = analyzer.classify_modules(func_reqs, knowledge_text)
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]✓ LLM 模块分类完成[/green]")
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]=重新分类 "
"[cyan]e[/cyan]=手动编辑 [cyan]ok[/cyan]=确认继续"
)
action = Prompt.ask("请选择操作", default="ok").strip().lower()
if action == "ok":
break
elif action == "r":
with console.status("[bold yellow]🤖 重新分类中...[/bold yellow]"):
try:
updates = analyzer.classify_modules(func_reqs, knowledge_text)
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
target = next((r for r in func_reqs if r.index_no == int(idx_str)), None)
if target is None:
console.print("[red]序号不存在[/red]")
continue
new_module = Prompt.ask(
f"新模块名(当前: {target.module}",
default=target.module or config.DEFAULT_MODULE,
)
target.module = new_module.strip() or config.DEFAULT_MODULE
db.update_functional_requirement(target)
console.print(f"[green]✓ '{target.function_name}'{target.module}[/green]")
print_module_summary(func_reqs)
return func_reqs
# ══════════════════════════════════════════════════════
# Step 5编辑功能需求
# ══════════════════════════════════════════════════════
def step_edit_requirements(
project: Project, func_reqs: List[FunctionalRequirement],
raw_req_id: int, non_interactive: bool = False, skip_indices: list = None,
) -> List[FunctionalRequirement]:
console.print(
"\n[bold]Step 5 · 编辑功能需求[/bold]"
+ (" [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":
idx_str = Prompt.ask("输入要删除的序号(多个用逗号分隔)")
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":
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)
new_req = FunctionalRequirement(
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,
)
new_req.id = db.create_functional_requirement(new_req)
func_reqs.append(new_req)
console.print(f"[green]✓ 已添加: {title}[/green]")
elif action == "e":
idx_str = Prompt.ask("输入要编辑的序号")
if not idx_str.isdigit():
continue
target = next((r for r in func_reqs if r.index_no == int(idx_str)), None)
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(
"新优先级", choices=["high","medium","low"], default=target.priority
)
target.module = Prompt.ask(
"新模块", default=target.module or config.DEFAULT_MODULE
).strip() or config.DEFAULT_MODULE
db.update_functional_requirement(target)
console.print(f"[green]✓ 已更新: {target.title}[/green]")
return func_reqs
# ══════════════════════════════════════════════════════
# Step 6A生成函数签名
# ══════════════════════════════════════════════════════
def step_generate_signatures(
project: Project, func_reqs: List[FunctionalRequirement],
output_dir: str, knowledge_text: str,
json_file_name: str = "function_signatures.json",
non_interactive: bool = False,
) -> tuple:
console.print(
"\n[bold]Step 6A · 生成函数签名 JSON[/bold]"
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
llm = LLMClient()
analyzer = RequirementAnalyzer(llm)
success_count = 0
fail_count = 0
def on_progress(index, total, req, signature, error):
nonlocal success_count, fail_count
if error:
console.print(
f" [{index}/{total}] [yellow]⚠ {req.title} 签名生成失败(降级): {error}[/yellow]"
)
fail_count += 1
else:
console.print(
f" [{index}/{total}] [green]✓ {req.title}[/green] "
f"[dim]{req.module}[/dim] → {signature.get('name')}()"
)
success_count += 1
console.print(f"[yellow]正在为 {len(func_reqs)} 个功能需求生成函数签名...[/yellow]")
signatures = analyzer.build_function_signatures_batch(
func_reqs=func_reqs, knowledge=knowledge_text, on_progress=on_progress,
)
report = validate_all_signatures(signatures)
if report:
console.print(f"[yellow]⚠ {len(report)} 个签名存在结构问题[/yellow]")
for fname, errs in report.items():
for err in errs:
console.print(f" [yellow]· {fname}: {err}[/yellow]")
else:
console.print("[green]✓ 所有签名结构校验通过[/green]")
json_path = write_function_signatures_json(
output_dir=output_dir, signatures=signatures,
project_name=project.name, project_description=project.description or "",
file_name=json_file_name,
)
console.print(
f"[green]✓ 签名 JSON 初版已写入: [cyan]{os.path.abspath(json_path)}[/cyan][/green]\n"
f" 成功: {success_count} 降级: {fail_count}"
)
return signatures, json_path
# ══════════════════════════════════════════════════════
# Step 6B生成代码文件
# ══════════════════════════════════════════════════════
def step_generate_code(
project: Project, func_reqs: List[FunctionalRequirement],
output_dir: str, knowledge_text: str, signatures: List[dict],
non_interactive: bool = False,
) -> Dict[str, str]:
console.print(
"\n[bold]Step 6B · 生成代码文件[/bold]"
+ (" [dim](非交互)[/dim]" if non_interactive else ""),
style="blue",
)
generator = CodeGenerator(LLMClient())
success_count = 0
fail_count = 0
func_name_to_url: Dict[str, str] = {}
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] "
f"[dim]{req.module}/{code_file.file_name}[/dim]"
)
success_count += 1
console.print(f"[yellow]开始生成 {len(func_reqs)} 个代码文件...[/yellow]")
generator.generate_batch(
func_reqs=func_reqs, output_dir=output_dir,
language=project.language, knowledge=knowledge_text,
signatures=signatures, on_progress=on_progress,
)
modules = list({req.module or config.DEFAULT_MODULE for req in func_reqs})
req_summary = "\n".join(
f"{i+1}. **{r.title}** (`{r.module}/{r.function_name}`) - {r.description[:80]}"
for i, r in enumerate(func_reqs)
)
write_project_readme(
output_dir=output_dir, project_name=project.name,
project_description=project.description or "",
requirements_summary=req_summary, modules=modules,
)
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
# ══════════════════════════════════════════════════════
# Step 6C回写 url 字段
# ══════════════════════════════════════════════════════
def step_patch_signatures_url(
project: Project, signatures: List[dict],
func_name_to_url: Dict[str, str], output_dir: str,
json_file_name: str, non_interactive: bool = False,
) -> str:
console.print(
"\n[bold]Step 6C · 回写代码路径url到签名 JSON[/bold]"
+ (" [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:
console.print(f"[yellow]⚠ {unpatched} 个函数 url 未回写[/yellow]")
print_signatures_preview(signatures)
json_path = write_function_signatures_json(
output_dir=output_dir, signatures=signatures,
project_name=project.name, project_description=project.description or "",
file_name=json_file_name,
)
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(
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,
):
"""Step 1 → 6C 完整工作流"""
print_banner()
# Step 1
project = step_init_project(
project_name=project_name, language=language,
description=description, non_interactive=non_interactive,
)
# Step 2
raw_text, knowledge_text, source_name, source_type = step_input_requirement(
project=project, requirement_text=requirement_text,
requirement_file=requirement_file,
knowledge_files=list(knowledge_files) if knowledge_files else [],
non_interactive=non_interactive,
)
# Step 3
raw_req_id, func_reqs = step_decompose_requirements(
project=project, raw_text=raw_text, knowledge_text=knowledge_text,
source_name=source_name, source_type=source_type,
non_interactive=non_interactive,
)
# Step 4
func_reqs = step_classify_modules(
project=project, func_reqs=func_reqs,
knowledge_text=knowledge_text, non_interactive=non_interactive,
)
# Step 5
func_reqs = step_edit_requirements(
project=project, func_reqs=func_reqs, raw_req_id=raw_req_id,
non_interactive=non_interactive, skip_indices=skip_indices or [],
)
if not func_reqs:
console.print("[red]⚠ 功能需求列表为空,流程终止[/red]")
return
output_dir = ensure_project_dir(project.name)
# Step 6A
signatures, json_path = step_generate_signatures(
project=project, func_reqs=func_reqs, output_dir=output_dir,
knowledge_text=knowledge_text, json_file_name=json_file_name,
non_interactive=non_interactive,
)
# Step 6B
func_name_to_url = step_generate_code(
project=project, func_reqs=func_reqs, output_dir=output_dir,
knowledge_text=knowledge_text, signatures=signatures,
non_interactive=non_interactive,
)
# Step 6C
json_path = step_patch_signatures_url(
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,
)
modules = sorted({req.module or config.DEFAULT_MODULE for req in func_reqs})
console.print(Panel(
f"[bold cyan]🎉 全部流程完成![/bold cyan]\n"
f"项目: [bold]{project.name}[/bold]\n"
f"描述: {project.description or '(无)'}\n"
f"模块: {', '.join(modules)}\n"
f"代码目录: [cyan]{os.path.abspath(output_dir)}[/cyan]\n"
f"签名文件: [cyan]{json_path}[/cyan]",
border_style="cyan",
))
# ══════════════════════════════════════════════════════
# CLI 根命令组
# ══════════════════════════════════════════════════════
@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,
help="以非交互模式运行")
@click.option("--project-name", "-p", default=None,
help="项目名称")
@click.option("--language", "-l", default=None,
type=click.Choice(["python","javascript","typescript","java","go","rust"]),
help=f"目标代码语言(默认: {config.DEFAULT_LANGUAGE}")
@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,
help="跳过的功能需求序号(可多次指定)")
@click.option("--json-file-name","-j", default="function_signatures.json",
help="签名 JSON 文件名")
def cmd_run(
non_interactive, project_name, language, description,
requirement_text, requirement_file, knowledge_file,
skip_index, json_file_name,
):
"""
运行完整工作流:需求分解 → 模块分类 → 代码生成。
\b
交互式运行:
python main.py run
\b
非交互式示例:
python main.py run --non-interactive \\
--project-name "UserSystem" \\
--language python \\
--description "用户管理系统后端" \\
--requirement-text "包含注册、登录、修改密码功能"
"""
try:
run_workflow(
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,
)
except KeyboardInterrupt:
console.print("\n[yellow]用户中断,退出[/yellow]")
sys.exit(0)
except Exception as e:
console.print(f"\n[bold red]❌ 错误: {e}[/bold red]")
import traceback; traceback.print_exc()
sys.exit(1)
# ══════════════════════════════════════════════════════
# 子命令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)
# ══════════════════════════════════════════════════════
# 入口
# ══════════════════════════════════════════════════════
if __name__ == "__main__":
cli()