#!/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 3:LLM 分解需求 # ══════════════════════════════════════════════════════ 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 [/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()