diff --git a/requirements_generator/main.py b/requirements_generator/main.py index f1e871e..231d2c7 100644 --- a/requirements_generator/main.py +++ b/requirements_generator/main.py @@ -12,7 +12,6 @@ from rich.table import Table from rich.panel import Panel from rich.prompt import Prompt, Confirm from rich.rule import Rule -from rich.text import Text import config from database.db_manager import DBManager @@ -30,7 +29,7 @@ from utils.output_writer import ( ) console = Console() -db = DBManager() +db = DBManager() # ══════════════════════════════════════════════════════ @@ -50,17 +49,17 @@ def print_banner(): def print_main_menu(): console.print(Rule("[bold cyan]主菜单[/bold cyan]")) menu_items = [ - ("1", "📁 新建项目", "输入需求 → 分解 → 生成代码"), - ("2", "🔄 变更项目需求", "变更已有需求 / 新增需求"), - ("3", "📋 查看所有项目", "列表展示全部项目"), - ("4", "🔍 查看项目详情", "需求列表 / 模块分组 / 变更历史"), - ("5", "🗑 删除指定项目", "删除项目及其所有数据"), - ("0", "🚪 退出", ""), + ("1", "📁 新建项目", "输入需求 → 分解 → 生成代码"), + ("2", "🔄 变更项目需求", "变更已有需求 / 变更模块需求 / 新增需求"), + ("3", "📋 查看所有项目", "列表展示全部项目"), + ("4", "🔍 查看项目详情", "需求列表 / 模块分组 / 变更历史"), + ("5", "🗑 删除指定项目", "删除项目及其所有数据"), + ("0", "🚪 退出", ""), ] table = Table(show_header=False, box=None, padding=(0, 2)) table.add_column("选项", style="bold yellow", width=4) - table.add_column("功能", style="bold", width=20) - table.add_column("说明", style="dim", width=35) + table.add_column("功能", style="bold", width=22) + table.add_column("说明", style="dim", width=38) for key, name, desc in menu_items: table.add_row(f"[{key}]", name, desc) console.print(table) @@ -68,47 +67,46 @@ def print_main_menu(): def print_projects_table(projects: List[Project]): - """以表格形式展示所有项目""" if not projects: console.print("[yellow] 暂无项目记录。[/yellow]") return table = Table(title="📋 项目列表", show_lines=True) - table.add_column("ID", style="cyan", width=6) - table.add_column("项目名", style="bold", width=22) - table.add_column("语言", style="magenta", width=12) - table.add_column("描述", width=35) - table.add_column("输出目录", style="dim", width=30) + table.add_column("ID", style="cyan", width=6) + table.add_column("项目名", style="bold", width=22) + table.add_column("语言", style="magenta", width=12) + table.add_column("描述", width=35) + table.add_column("输出目录", style="dim", width=30) for p in projects: + desc = p.description or "" table.add_row( str(p.id), p.name, p.language or "-", - (p.description or "")[:40] + ("..." if len(p.description or "") > 40 else ""), + desc[:40] + ("..." if len(desc) > 40 else ""), p.output_dir or "-", ) console.print(table) def print_functional_requirements(reqs: List[FunctionalRequirement]): - """以表格形式展示功能需求列表""" if not reqs: console.print("[yellow] 暂无功能需求。[/yellow]") return 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=10) - table.add_column("描述", width=35) - + 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=10) + table.add_column("描述", width=35) priority_color = {"high": "red", "medium": "yellow", "low": "green"} - status_color = {"pending": "yellow", "generated": "green", "failed": "red"} + status_color = {"pending": "yellow", "generated": "green", "failed": "red"} for req in reqs: pc = priority_color.get(req.priority, "white") sc = status_color.get(req.status, "white") + desc = req.description table.add_row( str(req.index_no), str(req.id) if req.id else "-", @@ -117,38 +115,57 @@ def print_functional_requirements(reqs: List[FunctionalRequirement]): f"[code]{req.function_name}[/code]", f"[{pc}]{req.priority}[/{pc}]", f"[{sc}]{req.status}[/{sc}]", - req.description[:40] + "..." if len(req.description) > 40 else req.description, + desc[:40] + "..." if len(desc) > 40 else desc, ) 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) + 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_module_list(reqs: List[FunctionalRequirement]) -> List[str]: + """打印带序号的模块列表,返回有序模块名列表。""" + module_map: Dict[str, List[FunctionalRequirement]] = {} + for req in reqs: + m = req.module or config.DEFAULT_MODULE + module_map.setdefault(m, []).append(req) + + table = Table(title="📦 模块列表", show_lines=True) + table.add_column("序号", style="cyan", width=6) + table.add_column("模块名", style="magenta bold", width=22) + table.add_column("需求数量", style="yellow", width=8) + table.add_column("包含函数", width=45) + + modules = sorted(module_map.keys()) + for i, module in enumerate(modules, 1): + funcs = [r.function_name for r in module_map[module]] + table.add_row(str(i), module, str(len(funcs)), ", ".join(funcs)) + console.print(table) + return modules + + 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) + 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", "") + 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", "-"), @@ -162,14 +179,13 @@ def print_signatures_preview(signatures: List[dict]): def print_change_history(histories: List[ChangeHistory]): - """展示变更历史""" if not histories: console.print("[dim] 暂无变更历史。[/dim]") return table = Table(title="🕒 变更历史", show_lines=True) - table.add_column("ID", style="cyan", width=6) - table.add_column("时间", style="dim", width=20) - table.add_column("变更内容", width=60) + table.add_column("ID", style="cyan", width=6) + table.add_column("时间", style="dim", width=20) + table.add_column("变更内容", width=60) for h in histories: table.add_row( str(h.id), @@ -180,7 +196,6 @@ def print_change_history(histories: List[ChangeHistory]): def select_project(prompt_text: str = "请选择项目 ID") -> Optional[Project]: - """列出所有项目并让用户选择,返回 Project 对象;取消返回 None""" projects = db.list_projects() if not projects: console.print("[yellow]当前暂无项目,请先新建项目。[/yellow]") @@ -236,28 +251,23 @@ def menu_create_project(): ) description = Prompt.ask("📝 项目描述(可选)", default="") project = Project( - name = project_name, - language = language, - output_dir = build_project_output_dir(project_name), - description = description, + 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]") # ── 2. 知识库(可选)──────────────────────────── - knowledge_text = "" - if Confirm.ask("是否加载知识库文件?", default=False): - kb_path = Prompt.ask("知识库文件路径(多个用逗号分隔)") - paths = [p.strip() for p in kb_path.split(",") if p.strip()] - knowledge_text = merge_knowledge_files(paths) - console.print(f"[dim]已加载 {len(paths)} 个知识库文件[/dim]") + knowledge_text = _load_knowledge_optional() # ── 3. 原始需求输入 ────────────────────────────── console.print("\n[bold]请选择需求来源:[/bold]") source = Prompt.ask("来源", choices=["text", "file"], default="text") if source == "file": file_path = Prompt.ask("需求文件路径") - raw_text = read_file_auto(file_path) + raw_text = read_file_auto(file_path) source_name = file_path source_type = "file" else: @@ -268,7 +278,7 @@ def menu_create_project(): if line == "": break lines.append(line) - raw_text = "\n".join(lines).strip() + raw_text = "\n".join(lines).strip() source_name = "" source_type = "text" @@ -277,30 +287,29 @@ def menu_create_project(): return raw_req = RawRequirement( - project_id = project.id, - content = raw_text, - source_type = source_type, - source_name = source_name, - knowledge = knowledge_text, + project_id=project.id, + content=raw_text, + source_type=source_type, + source_name=source_name, + knowledge=knowledge_text, ) raw_req_id = db.create_raw_requirement(raw_req) console.print(f"[green]✓ 原始需求已保存 (ID={raw_req_id})[/green]") # ── 4. LLM 分解功能需求 ────────────────────────── - llm = LLMClient() + llm = LLMClient() analyzer = RequirementAnalyzer(llm) with console.status("[cyan]LLM 正在分解需求...[/cyan]"): func_reqs = analyzer.decompose( - raw_requirement = raw_text, - project_id = project.id, - raw_req_id = raw_req_id, - knowledge = knowledge_text, + raw_requirement=raw_text, + project_id=project.id, + raw_req_id=raw_req_id, + knowledge=knowledge_text, ) console.print(f"[green]✓ 分解完成,共 {len(func_reqs)} 条功能需求[/green]") print_functional_requirements(func_reqs) - # 交互式编辑分解结果 while True: action = Prompt.ask( "操作", @@ -316,7 +325,6 @@ def menu_create_project(): elif action == "add": func_reqs = _interactive_add_req(func_reqs, project, raw_req_id) - # 持久化功能需求 for req in func_reqs: req.id = db.create_functional_requirement(req) console.print(f"[green]✓ 功能需求已持久化,共 {len(func_reqs)} 条[/green]") @@ -334,22 +342,18 @@ def menu_create_project(): if Confirm.ask("是否手动调整模块归属?", default=False): func_reqs = _interactive_adjust_modules(func_reqs) - # ── 6. 确保输出目录 ────────────────────────────── + # ── 6. 输出目录 ────────────────────────────────── output_dir = ensure_project_dir(project.name) # ── 7. 生成函数签名 ────────────────────────────── console.print("\n[bold]Step · 生成函数签名[/bold]", style="blue") - signatures = _generate_signatures( - analyzer = analyzer, - func_reqs = func_reqs, - knowledge_text = knowledge_text, - ) + signatures = _generate_signatures(analyzer, func_reqs, knowledge_text) json_path = write_function_signatures_json( - output_dir = output_dir, - signatures = signatures, - project_name = project.name, - project_description = project.description or "", - file_name = "function_signatures.json", + output_dir=output_dir, + signatures=signatures, + project_name=project.name, + project_description=project.description or "", + file_name="function_signatures.json", ) console.print(f"[green]✓ 签名 JSON 已写入: {json_path}[/green]") print_signatures_preview(signatures) @@ -360,14 +364,14 @@ def menu_create_project(): # ── 8. 生成代码文件 ────────────────────────────── console.print("\n[bold]Step · 生成代码文件[/bold]", style="blue") - generator = CodeGenerator(llm) + generator = CodeGenerator(llm) code_files = _generate_code( - generator = generator, - project = project, - func_reqs = func_reqs, - output_dir = output_dir, - knowledge_text = knowledge_text, - signatures = signatures, + generator=generator, + project=project, + func_reqs=func_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, + signatures=signatures, ) for cf in code_files: db.upsert_code_file(cf) @@ -381,11 +385,11 @@ def menu_create_project(): # ── 10. 回填签名 URL ───────────────────────────── _patch_and_save_signatures( - project = project, - signatures = signatures, - code_files = code_files, - output_dir = output_dir, - file_name = "function_signatures.json", + project=project, + signatures=signatures, + code_files=code_files, + output_dir=output_dir, + file_name="function_signatures.json", ) console.print(Panel.fit( @@ -414,35 +418,58 @@ def menu_change_requirements(): return output_dir = ensure_project_dir(project.name) - - # 知识库(可选复用) knowledge_text = _load_knowledge_optional() + change_menu = [ + ("A", "🔧 变更指定功能需求", "按需求 ID 修改单条需求"), + ("B", "📦 变更指定模块需求", "按模块批量操作模块内所有需求"), + ("C", "➕ 新增需求", "输入新需求文本,LLM 分解后生成代码"), + ] + while True: console.print() print_functional_requirements(func_reqs) - console.print(Rule()) + console.print(Rule("[bold]变更操作[/bold]")) + + table = Table(show_header=False, box=None, padding=(0, 2)) + table.add_column("选项", style="bold yellow", width=5) + table.add_column("功能", style="bold", width=22) + table.add_column("说明", style="dim", width=38) + for key, name, desc in change_menu: + table.add_row(f"[{key}]", name, desc) + table.add_row("[back]", "↩ 返回主菜单", "") + console.print(table) + action = Prompt.ask( "请选择操作", - choices=["A-变更已有需求", "B-新增需求", "back"], + choices=["A", "B", "C", "back"], default="back", ) - if action == "A-变更已有需求": + if action == "A": _change_existing_requirement( - project = project, - func_reqs = func_reqs, - output_dir = output_dir, - knowledge_text = knowledge_text, + project=project, + func_reqs=func_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, ) func_reqs = db.list_functional_requirements(project.id) - elif action == "B-新增需求": + elif action == "B": + _change_module_requirements( + project=project, + func_reqs=func_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, + ) + func_reqs = db.list_functional_requirements(project.id) + + elif action == "C": _add_new_requirements( - project = project, - func_reqs = func_reqs, - output_dir = output_dir, - knowledge_text = knowledge_text, + project=project, + func_reqs=func_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, ) func_reqs = db.list_functional_requirements(project.id) @@ -474,7 +501,6 @@ def menu_project_detail(): if not project: return - # 项目基本信息 console.print(Panel( f"[bold]项目名称:[/bold]{project.name}\n" f"[bold]语言: [/bold]{project.language}\n" @@ -484,21 +510,17 @@ def menu_project_detail(): border_style="cyan", )) - # 功能需求列表 func_reqs = db.list_functional_requirements(project.id) print_functional_requirements(func_reqs) - # 模块分组 if func_reqs: print_module_summary(func_reqs) - # 变更历史 - llm = LLMClient() + llm = LLMClient() analyzer = RequirementAnalyzer(llm) histories = analyzer.get_change_history(project.id) print_change_history(histories) - # 签名 JSON 预览 if project.output_dir: sig_path = os.path.join(project.output_dir, "function_signatures.json") if os.path.exists(sig_path): @@ -532,14 +554,13 @@ def menu_delete_project(): )) if not Confirm.ask( - f"[bold red]确认删除项目 '{project.name}'(ID={project.id})?此操作不可恢复![/bold red]", - default=False, + f"[bold red]确认删除项目 '{project.name}'(ID={project.id})?此操作不可恢复![/bold red]", + default=False, ): console.print("[yellow]已取消删除。[/yellow]") _pause() return - # 二次确认 confirm_name = Prompt.ask("请再次输入项目名称以确认") if confirm_name != project.name: console.print("[red]项目名称不匹配,删除已取消。[/red]") @@ -552,15 +573,16 @@ def menu_delete_project(): # ══════════════════════════════════════════════════════ -# 变更已有需求(内部) +# 变更操作 A:变更指定功能需求 # ══════════════════════════════════════════════════════ def _change_existing_requirement( - project: Project, - func_reqs: List[FunctionalRequirement], - output_dir: str, - knowledge_text: str = "", + project: Project, + func_reqs: List[FunctionalRequirement], + output_dir: str, + knowledge_text: str = "", ) -> None: + """按需求 ID 修改单条功能需求,确认后重新生成签名与代码。""" req_id_str = Prompt.ask("请输入要变更的功能需求 ID") try: req_id = int(req_id_str) @@ -574,17 +596,17 @@ def _change_existing_requirement( return console.print(f"\n当前需求: [bold]{req.title}[/bold](ID={req.id})") - new_description = Prompt.ask(" 新描述", default=req.description) - new_priority = Prompt.ask( + new_description = Prompt.ask(" 新描述", default=req.description) + new_priority = Prompt.ask( " 新优先级", default=req.priority, choices=["high", "medium", "low"], ) new_module = Prompt.ask(" 新模块名", default=req.module) changed = ( - new_description != req.description - or new_priority != req.priority - or new_module != req.module + new_description != req.description + or new_priority != req.priority + or new_module != req.module ) if not changed: console.print("[dim]未检测到任何变更,跳过。[/dim]") @@ -602,39 +624,37 @@ def _change_existing_requirement( console.print("[yellow]已取消变更。[/yellow]") return - llm = LLMClient() + llm = LLMClient() analyzer = RequirementAnalyzer(llm) analyzer.log_change(project.id, change_summary) req.description = new_description - req.priority = new_priority - req.module = new_module - req.status = "pending" + req.priority = new_priority + req.module = new_module + req.status = "pending" db.update_functional_requirement(req) console.print(f"[green]✓ 需求 ID={req.id} 已更新[/green]") - # 重新生成签名与代码 console.print("\n[cyan]正在重新生成签名与代码...[/cyan]") change_sig_file = f"change_{req.id}_signatures.json" - signatures = _generate_signatures(analyzer, [req], knowledge_text) - json_path = write_function_signatures_json( - output_dir = output_dir, - signatures = signatures, - project_name = project.name, - project_description = project.description or "", - file_name = change_sig_file, + write_function_signatures_json( + output_dir=output_dir, + signatures=signatures, + project_name=project.name, + project_description=project.description or "", + file_name=change_sig_file, ) print_signatures_preview(signatures) - generator = CodeGenerator(llm) + generator = CodeGenerator(llm) code_files = _generate_code( - generator = generator, - project = project, - func_reqs = [req], - output_dir = output_dir, - knowledge_text = knowledge_text, - signatures = signatures, + generator=generator, + project=project, + func_reqs=[req], + output_dir=output_dir, + knowledge_text=knowledge_text, + signatures=signatures, ) for cf in code_files: db.upsert_code_file(cf) @@ -642,26 +662,354 @@ def _change_existing_requirement( db.update_functional_requirement(req) _patch_and_save_signatures( - project = project, - signatures = signatures, - code_files = code_files, - output_dir = output_dir, - file_name = change_sig_file, + project=project, + signatures=signatures, + code_files=code_files, + output_dir=output_dir, + file_name=change_sig_file, ) - # 合并到主签名 JSON _merge_signatures_to_main(project, signatures, output_dir) console.print(f"[green]✓ 变更完成,代码已重新生成[/green]") # ══════════════════════════════════════════════════════ -# 新增需求(内部) +# 变更操作 B:变更指定模块需求 +# ══════════════════════════════════════════════════════ + +def _change_module_requirements( + project: Project, + func_reqs: List[FunctionalRequirement], + output_dir: str, + knowledge_text: str = "", +) -> None: + """ + 选择目标模块,进入模块级变更子菜单: + B1 · 重命名模块 + B2 · 逐条修改模块内需求 + B3 · 批量重新生成模块代码 + """ + console.print(Rule("[bold magenta]变更指定模块需求[/bold magenta]")) + + # ── 选择模块 ───────────────────────────────────── + modules = print_module_list(func_reqs) + if not modules: + console.print("[yellow]当前项目暂无模块信息。[/yellow]") + return + + mod_idx_str = Prompt.ask("请输入模块序号(输入 0 返回)", default="0") + if mod_idx_str == "0": + return + try: + mod_idx = int(mod_idx_str) - 1 + if mod_idx < 0 or mod_idx >= len(modules): + raise ValueError + target_module = modules[mod_idx] + except ValueError: + console.print("[red]序号无效[/red]") + return + + # 该模块下的所有需求 + module_reqs = [ + r for r in func_reqs + if (r.module or config.DEFAULT_MODULE) == target_module + ] + console.print( + f"\n[bold magenta]模块:{target_module}[/bold magenta]" + f" 共 {len(module_reqs)} 条需求" + ) + print_functional_requirements(module_reqs) + + # ── 模块级操作子菜单 ───────────────────────────── + sub_menu = [ + ("B1", "✏️ 重命名模块", "修改模块名,批量更新该模块所有需求"), + ("B2", "🔧 逐条修改模块内需求", "对模块内每条需求单独修改并重新生成"), + ("B3", "⚡ 批量重新生成模块代码", "保持需求内容不变,整体重新生成签名+代码"), + ] + + while True: + console.print(Rule()) + table = Table(show_header=False, box=None, padding=(0, 2)) + table.add_column("选项", style="bold yellow", width=5) + table.add_column("功能", style="bold", width=24) + table.add_column("说明", style="dim", width=38) + for key, name, desc in sub_menu: + table.add_row(f"[{key}]", name, desc) + table.add_row("[back]", "↩ 返回", "") + console.print(table) + + sub_action = Prompt.ask( + "请选择操作", + choices=["B1", "B2", "B3", "back"], + default="back", + ) + + if sub_action == "B1": + _module_rename( + project=project, + module_reqs=module_reqs, + old_module=target_module, + ) + # 模块名已变,退出子菜单 + break + + elif sub_action == "B2": + _module_edit_each( + project=project, + module_reqs=module_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, + ) + # 刷新 module_reqs + module_reqs = [ + db.get_functional_requirement(r.id) + for r in module_reqs + ] + + elif sub_action == "B3": + _module_regen_code( + project=project, + module_reqs=module_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, + ) + + else: + break + + +# ── B1:重命名模块 ──────────────────────────────────── + +def _module_rename( + project: Project, + module_reqs: List[FunctionalRequirement], + old_module: str, +) -> None: + """将指定模块重命名,批量更新该模块下所有需求的 module 字段。""" + new_module = Prompt.ask(f"请输入新模块名(当前:{old_module})") + if not new_module.strip(): + console.print("[red]模块名不能为空[/red]") + return + if new_module == old_module: + console.print("[dim]模块名未变更,跳过。[/dim]") + return + + change_summary = ( + f"模块重命名:'{old_module}' → '{new_module}'\n" + f" 影响需求数:{len(module_reqs)} 条\n" + + "\n".join(f" · [{r.id}] {r.title}" for r in module_reqs) + ) + console.print(Panel(change_summary, title="📝 模块重命名预览", border_style="yellow")) + + if not Confirm.ask("确认执行模块重命名?", default=True): + console.print("[yellow]已取消。[/yellow]") + return + + llm = LLMClient() + analyzer = RequirementAnalyzer(llm) + analyzer.log_change(project.id, change_summary) + + for req in module_reqs: + req.module = new_module + db.update_functional_requirement(req) + + console.print( + f"[green]✓ 模块已重命名:'{old_module}' → '{new_module}'," + f"共更新 {len(module_reqs)} 条需求[/green]" + ) + + +# ── B2:逐条修改模块内需求 ──────────────────────────── + +def _module_edit_each( + project: Project, + module_reqs: List[FunctionalRequirement], + output_dir: str, + knowledge_text: str = "", +) -> None: + """ + 对模块内需求逐条展示并允许修改, + 每条修改确认后立即重新生成签名与代码。 + """ + llm = LLMClient() + analyzer = RequirementAnalyzer(llm) + generator = CodeGenerator(llm) + + for req in module_reqs: + console.print(Rule(f"[cyan]需求 ID={req.id} · {req.title}[/cyan]")) + console.print( + f" 描述: {req.description}\n" + f" 优先级: {req.priority}\n" + f" 模块: {req.module}" + ) + + if not Confirm.ask("是否修改此需求?", default=False): + continue + + new_description = Prompt.ask(" 新描述", default=req.description) + new_priority = Prompt.ask( + " 新优先级", default=req.priority, + choices=["high", "medium", "low"], + ) + new_module = Prompt.ask(" 新模块名", default=req.module) + + changed = ( + new_description != req.description + or new_priority != req.priority + or new_module != req.module + ) + if not changed: + console.print("[dim] 未检测到变更,跳过。[/dim]") + continue + + change_summary = ( + f"需求 '{req.title}'(ID={req.id})变更:\n" + f" 描述: '{req.description}' → '{new_description}'\n" + f" 优先级: '{req.priority}' → '{new_priority}'\n" + f" 模块: '{req.module}' → '{new_module}'" + ) + console.print(Panel(change_summary, title="📝 变更预览", border_style="yellow")) + + if not Confirm.ask("确认此条变更?", default=True): + console.print("[yellow] 已跳过。[/yellow]") + continue + + analyzer.log_change(project.id, change_summary) + req.description = new_description + req.priority = new_priority + req.module = new_module + req.status = "pending" + db.update_functional_requirement(req) + + console.print(f" [cyan]正在重新生成 {req.function_name} 的签名与代码...[/cyan]") + change_sig_file = f"change_{req.id}_signatures.json" + signatures = _generate_signatures(analyzer, [req], knowledge_text) + write_function_signatures_json( + output_dir=output_dir, + signatures=signatures, + project_name=project.name, + project_description=project.description or "", + file_name=change_sig_file, + ) + code_files = _generate_code( + generator=generator, + project=project, + func_reqs=[req], + output_dir=output_dir, + knowledge_text=knowledge_text, + signatures=signatures, + ) + for cf in code_files: + db.upsert_code_file(cf) + req.status = "generated" + db.update_functional_requirement(req) + _patch_and_save_signatures( + project=project, + signatures=signatures, + code_files=code_files, + output_dir=output_dir, + file_name=change_sig_file, + ) + _merge_signatures_to_main(project, signatures, output_dir) + console.print(f" [green]✓ 需求 ID={req.id} 变更完成[/green]") + + console.print("[green]✓ 模块内需求逐条修改完成[/green]") + + +# ── B3:批量重新生成模块代码 ────────────────────────── + +def _module_regen_code( + project: Project, + module_reqs: List[FunctionalRequirement], + output_dir: str, + knowledge_text: str = "", +) -> None: + """ + 保持需求内容不变,对整个模块的所有需求 + 批量重新生成函数签名与代码文件,并更新主签名 JSON。 + """ + module_name = module_reqs[0].module if module_reqs else "未知模块" + console.print(Panel( + f"模块:[bold magenta]{module_name}[/bold magenta]\n" + f"需求数量:{len(module_reqs)} 条\n" + + "\n".join( + f" · [{r.id}] {r.title} ({r.function_name})" + for r in module_reqs + ), + title="⚡ 批量重新生成预览", + border_style="cyan", + )) + + if not Confirm.ask( + f"确认对模块 '{module_name}' 的 {len(module_reqs)} 条需求批量重新生成代码?", + default=True, + ): + console.print("[yellow]已取消。[/yellow]") + return + + llm = LLMClient() + analyzer = RequirementAnalyzer(llm) + generator = CodeGenerator(llm) + + for req in module_reqs: + req.status = "pending" + db.update_functional_requirement(req) + + console.print(f"\n[cyan]正在为模块 '{module_name}' 生成函数签名...[/cyan]") + signatures = _generate_signatures(analyzer, module_reqs, knowledge_text) + mod_sig_file = f"module_{module_name}_signatures.json" + write_function_signatures_json( + output_dir=output_dir, + signatures=signatures, + project_name=project.name, + project_description=project.description or "", + file_name=mod_sig_file, + ) + print_signatures_preview(signatures) + + console.print(f"\n[cyan]正在为模块 '{module_name}' 生成代码文件...[/cyan]") + code_files = _generate_code( + generator=generator, + project=project, + func_reqs=module_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, + signatures=signatures, + ) + for cf in code_files: + db.upsert_code_file(cf) + for req in module_reqs: + req.status = "generated" + db.update_functional_requirement(req) + + _patch_and_save_signatures( + project=project, + signatures=signatures, + code_files=code_files, + output_dir=output_dir, + file_name=mod_sig_file, + ) + _merge_signatures_to_main(project, signatures, output_dir) + + change_summary = ( + f"模块 '{module_name}' 批量重新生成代码,共 {len(module_reqs)} 条需求" + ) + analyzer.log_change(project.id, change_summary) + + console.print( + f"[bold green]✅ 模块 '{module_name}' 批量重新生成完成!" + f"共 {len(code_files)} 个文件[/bold green]" + ) + + +# ══════════════════════════════════════════════════════ +# 变更操作 C:新增需求 # ══════════════════════════════════════════════════════ def _add_new_requirements( - project: Project, - func_reqs: List[FunctionalRequirement], - output_dir: str, - knowledge_text: str = "", + project: Project, + func_reqs: List[FunctionalRequirement], + output_dir: str, + knowledge_text: str = "", ) -> None: raw_reqs = db.get_raw_requirement(project.id) if not raw_reqs: @@ -681,27 +1029,27 @@ def _add_new_requirements( console.print("[yellow]新增需求内容为空,已取消。[/yellow]") return - llm = LLMClient() + llm = LLMClient() analyzer = RequirementAnalyzer(llm) with console.status("[cyan]LLM 正在分解新增需求...[/cyan]"): new_reqs = analyzer.add_new_requirements( - new_requirement_text = new_req_text, - project_id = project.id, - raw_req_id = raw_req_id, - existing_reqs = func_reqs, - knowledge = knowledge_text, + new_requirement_text=new_req_text, + project_id=project.id, + raw_req_id=raw_req_id, + existing_reqs=func_reqs, + knowledge=knowledge_text, ) console.print(f"[green]✓ 分解完成,新增 {len(new_reqs)} 条功能需求[/green]") print_functional_requirements(new_reqs) change_summary = ( - f"新增 {len(new_reqs)} 条功能需求:\n" - + "\n".join( - f" + [{r.index_no}] {r.title} ({r.function_name})" - for r in new_reqs - ) + f"新增 {len(new_reqs)} 条功能需求:\n" + + "\n".join( + f" + [{r.index_no}] {r.title} ({r.function_name})" + for r in new_reqs + ) ) console.print(Panel(change_summary, title="📝 新增需求预览", border_style="green")) @@ -709,7 +1057,6 @@ def _add_new_requirements( console.print("[yellow]已取消新增。[/yellow]") return - # 模块分类 with console.status("[cyan]LLM 正在进行模块分类...[/cyan]"): module_map = analyzer.classify_modules(new_reqs, knowledge_text) name_to_module = {item["function_name"]: item["module"] for item in module_map} @@ -718,34 +1065,29 @@ def _add_new_requirements( req.module = name_to_module[req.function_name] print_module_summary(new_reqs) - # 持久化 for req in new_reqs: req.id = db.create_functional_requirement(req) console.print(f"[green]✓ 新增功能需求已持久化[/green]") - - # 记录变更历史 analyzer.log_change(project.id, change_summary) - # 生成签名 signatures = _generate_signatures(analyzer, new_reqs, knowledge_text) write_function_signatures_json( - output_dir = output_dir, - signatures = signatures, - project_name = project.name, - project_description = project.description or "", - file_name = "function_signatures_new.json", + output_dir=output_dir, + signatures=signatures, + project_name=project.name, + project_description=project.description or "", + file_name="function_signatures_new.json", ) print_signatures_preview(signatures) - # 生成代码 - generator = CodeGenerator(llm) + generator = CodeGenerator(llm) code_files = _generate_code( - generator = generator, - project = project, - func_reqs = new_reqs, - output_dir = output_dir, - knowledge_text = knowledge_text, - signatures = signatures, + generator=generator, + project=project, + func_reqs=new_reqs, + output_dir=output_dir, + knowledge_text=knowledge_text, + signatures=signatures, ) for cf in code_files: db.upsert_code_file(cf) @@ -753,17 +1095,15 @@ def _add_new_requirements( req.status = "generated" db.update_functional_requirement(req) - # 更新 README all_reqs = db.list_functional_requirements(project.id) _write_readme(project, all_reqs, output_dir) - # 回填 URL & 合并主签名 _patch_and_save_signatures( - project = project, - signatures = signatures, - code_files = code_files, - output_dir = output_dir, - file_name = "function_signatures_new.json", + project=project, + signatures=signatures, + code_files=code_files, + output_dir=output_dir, + file_name="function_signatures_new.json", ) _merge_signatures_to_main(project, signatures, output_dir) @@ -777,24 +1117,20 @@ def _add_new_requirements( # ══════════════════════════════════════════════════════ def _load_knowledge_optional() -> str: - """可选加载知识库文件,返回合并后的文本""" if Confirm.ask("是否加载知识库文件?", default=False): kb_path = Prompt.ask("知识库文件路径(多个用逗号分隔)") - paths = [p.strip() for p in kb_path.split(",") if p.strip()] - text = merge_knowledge_files(paths) + paths = [p.strip() for p in kb_path.split(",") if p.strip()] + text = merge_knowledge_files(paths) console.print(f"[dim]已加载 {len(paths)} 个知识库文件[/dim]") return text return "" def _generate_signatures( - analyzer: RequirementAnalyzer, - func_reqs: List[FunctionalRequirement], - knowledge_text: str = "", + analyzer: RequirementAnalyzer, + func_reqs: List[FunctionalRequirement], + knowledge_text: str = "", ) -> List[dict]: - """批量生成函数签名,带进度输出""" - total = len(func_reqs) - def on_progress(i, t, req, sig, err): status = "[red]✗ 降级[/red]" if err else "[green]✓[/green]" console.print( @@ -804,21 +1140,20 @@ def _generate_signatures( ) return analyzer.build_function_signatures_batch( - func_reqs = func_reqs, - knowledge = knowledge_text, - on_progress = on_progress, + func_reqs=func_reqs, + knowledge=knowledge_text, + on_progress=on_progress, ) def _generate_code( - generator: CodeGenerator, - project: Project, - func_reqs: List[FunctionalRequirement], - output_dir: str, - knowledge_text: str = "", - signatures: List[dict] = None, + generator: CodeGenerator, + project: Project, + func_reqs: List[FunctionalRequirement], + output_dir: str, + knowledge_text: str = "", + signatures: List[dict] = None, ) -> list: - """批量生成代码文件,带进度输出""" def on_progress(i, t, req, code_file, err): if err: console.print( @@ -831,42 +1166,42 @@ def _generate_code( ) return generator.generate_batch( - func_reqs = func_reqs, - output_dir = output_dir, - language = project.language, - knowledge = knowledge_text, - signatures = signatures, - on_progress = on_progress, + func_reqs=func_reqs, + output_dir=output_dir, + language=project.language, + knowledge=knowledge_text, + signatures=signatures, + on_progress=on_progress, ) def _write_readme( - project: Project, - func_reqs: List[FunctionalRequirement], - output_dir: str, + project: Project, + func_reqs: List[FunctionalRequirement], + output_dir: str, ) -> str: req_lines = "\n".join( - f"{i+1}. **{r.title}** (`{r.function_name}`) — {r.description}" + f"{i + 1}. **{r.title}** (`{r.function_name}`) — {r.description}" for i, r in enumerate(func_reqs) ) - modules = list({r.module for r in func_reqs if r.module}) - path = write_project_readme( - output_dir = output_dir, - project_name = project.name, - project_description = project.description or "", - requirements_summary = req_lines, - modules = modules, + modules = list({r.module for r in func_reqs if r.module}) + path = write_project_readme( + output_dir=output_dir, + project_name=project.name, + project_description=project.description or "", + requirements_summary=req_lines, + modules=modules, ) console.print(f"[green]✓ README 已生成: {path}[/green]") return path def _patch_and_save_signatures( - project: Project, - signatures: List[dict], - code_files: list, - output_dir: str, - file_name: str = "function_signatures.json", + project: Project, + signatures: List[dict], + code_files: list, + output_dir: str, + file_name: str = "function_signatures.json", ) -> str: func_name_to_url = { cf.file_name.replace(_get_ext(project.language), ""): cf.file_path @@ -874,23 +1209,22 @@ def _patch_and_save_signatures( } patch_signatures_with_url(signatures, func_name_to_url) path = write_function_signatures_json( - output_dir = output_dir, - signatures = signatures, - project_name = project.name, - project_description = project.description or "", - file_name = file_name, + output_dir=output_dir, + signatures=signatures, + project_name=project.name, + project_description=project.description or "", + file_name=file_name, ) console.print(f"[green]✓ 签名 URL 已回填: {path}[/green]") return path def _merge_signatures_to_main( - project: Project, - new_sigs: List[dict], - output_dir: str, - main_file: str = "function_signatures.json", + project: Project, + new_sigs: List[dict], + output_dir: str, + main_file: str = "function_signatures.json", ) -> None: - """将新签名合并追加到主签名 JSON(以 name 去重,新覆盖旧)""" main_path = os.path.join(output_dir, main_file) if os.path.exists(main_path): with open(main_path, "r", encoding="utf-8") as f: @@ -899,16 +1233,14 @@ def _merge_signatures_to_main( else: existing_sigs = [] main_doc = { - "project": project.name, + "project": project.name, "description": project.description or "", - "functions": [], + "functions": [], } - sig_map = {s["name"]: s for s in existing_sigs} for sig in new_sigs: sig_map[sig["name"]] = sig main_doc["functions"] = list(sig_map.values()) - with open(main_path, "w", encoding="utf-8") as f: json.dump(main_doc, f, ensure_ascii=False, indent=2) console.print( @@ -918,15 +1250,15 @@ def _merge_signatures_to_main( def _interactive_edit_req( - reqs: List[FunctionalRequirement], + reqs: List[FunctionalRequirement], ) -> List[FunctionalRequirement]: idx = int(Prompt.ask("输入要编辑的序号")) for req in reqs: if req.index_no == idx: - req.title = Prompt.ask("新标题", default=req.title) - req.description = Prompt.ask("新描述", default=req.description) + req.title = Prompt.ask("新标题", default=req.title) + req.description = Prompt.ask("新描述", default=req.description) req.function_name = Prompt.ask("新函数名", default=req.function_name) - req.priority = Prompt.ask( + req.priority = Prompt.ask( "新优先级", default=req.priority, choices=["high", "medium", "low"], ) @@ -938,9 +1270,9 @@ def _interactive_edit_req( def _interactive_delete_req( - reqs: List[FunctionalRequirement], + reqs: List[FunctionalRequirement], ) -> List[FunctionalRequirement]: - idx = int(Prompt.ask("输入要删除的序号")) + idx = int(Prompt.ask("输入要删除的序号")) new_reqs = [r for r in reqs if r.index_no != idx] if len(new_reqs) < len(reqs): console.print(f"[green]✓ 序号 {idx} 已删除[/green]") @@ -950,29 +1282,29 @@ def _interactive_delete_req( def _interactive_add_req( - reqs: List[FunctionalRequirement], - project: Project, - raw_req_id: int, + reqs: List[FunctionalRequirement], + project: Project, + raw_req_id: int, ) -> List[FunctionalRequirement]: - next_idx = max((r.index_no for r in reqs), default=0) + 1 - title = Prompt.ask("新需求标题") - description = Prompt.ask("新需求描述") + next_idx = max((r.index_no for r in reqs), default=0) + 1 + title = Prompt.ask("新需求标题") + description = Prompt.ask("新需求描述") function_name = Prompt.ask("函数名(snake_case)") - priority = Prompt.ask( + priority = Prompt.ask( "优先级", choices=["high", "medium", "low"], default="medium", ) - module = Prompt.ask("所属模块", default=config.DEFAULT_MODULE) + module = Prompt.ask("所属模块", default=config.DEFAULT_MODULE) new_req = FunctionalRequirement( - project_id = project.id, - raw_req_id = raw_req_id, - index_no = next_idx, - title = title, - description = description, - function_name = function_name, - priority = priority, - module = module, - status = "pending", - is_custom = True, + project_id=project.id, + raw_req_id=raw_req_id, + index_no=next_idx, + title=title, + description=description, + function_name=function_name, + priority=priority, + module=module, + status="pending", + is_custom=True, ) reqs.append(new_req) console.print(f"[green]✓ 已添加新需求: {title}(序号 {next_idx})[/green]") @@ -980,7 +1312,7 @@ def _interactive_add_req( def _interactive_adjust_modules( - reqs: List[FunctionalRequirement], + reqs: List[FunctionalRequirement], ) -> List[FunctionalRequirement]: while True: idx = Prompt.ask("输入要调整的需求序号(输入 0 结束)", default="0") @@ -1003,7 +1335,6 @@ def _interactive_adjust_modules( def _pause(): - """等待用户按 Enter 键继续""" console.print() input(" 按 Enter 键返回主菜单...") @@ -1049,4 +1380,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main()