#!/usr/bin/env python3 """ LLM 代码生成工具的命令行接口 支持 init、enhance、fix、check、design 五种操作模式,使用 typer 构建 CLI。 """ from pathlib import Path from typing import Optional import sys import typer from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn from loguru import logger from .init_generator import InitGenerator from .enhance_generator import EnhanceGenerator from .fix_generator import FixGenerator from .design_generator import DesignGenerator # 新增导入DesignGenerator app = typer.Typer(help="基于LLM的自动化代码生成与维护工具") console = Console() def init_logging( output_dir: Path, log_file: Optional[str] = None, command_name: str = "cli" ) -> str: """初始化日志配置到logs/目录""" log_dir = output_dir / "logs" log_dir.mkdir(parents=True, exist_ok=True) if log_file is None: log_file = str(log_dir / f"{command_name}.log") logger.remove() # 移除默认handler logger.add(sys.stderr, level="WARNING") # 控制台输出WARNING及以上 logger.add(log_file, rotation="10 MB", level="DEBUG") # 文件记录DEBUG logger.info(f"日志已初始化到: {log_file}") return log_file @app.command() def init( readme: Path = typer.Argument( ..., exists=True, file_okay=True, dir_okay=False, help="README.md 文件路径" ), output_dir: Optional[Path] = typer.Option( None, "--output", "-o", help="输出根目录,默认为当前目录" ), api_key: Optional[str] = typer.Option( None, "--api-key", envvar="DEEPSEEK_APIKEY", help="API密钥" ), base_url: str = typer.Option( "https://api.deepseek.com", "--base-url", help="API基础URL" ), model: str = typer.Option("deepseek-reasoner", "--model", "-m", help="使用的模型"), log_file: Optional[str] = typer.Option(None, "--log", help="日志文件路径"), max_concurrency: int = typer.Option( 4, "--max-concurrency", help="并发生成的最大工作线程数,默认4" ), ): """初始化项目:根据 README.md 自动生成完整的代码。""" if output_dir is None: output_dir = Path.cwd() # 初始化日志配置 log_file_path = init_logging(output_dir, log_file, command_name="init") # 处理致命错误:检查README文件存在性(已由typer处理),其他错误在try块中捕获 try: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), console=console, ) as progress: task_id = progress.add_task("正在初始化项目...", total=1) # 修改:设置总任务数为1以控制进度显示 generator = InitGenerator( api_key=api_key, base_url=base_url, model=model, output_dir=str(output_dir), log_file=log_file_path, max_concurrency=max_concurrency, ) generator.run(readme) progress.update(task_id, completed=1, description="初始化完成") # 修改:更新完成状态 # 调用core.CodeGenerator.run并显示最终统计信息(假设从日志或生成器状态获取) console.print("[green]生成完成。成功处理文件,详情请查看日志。[/green]") except Exception as e: logger.error(f"初始化失败: {e}") raise typer.Exit(code=1) @app.command() def enhance( issue_file: Path = typer.Argument( ..., exists=True, file_okay=True, dir_okay=False, help="需求工单文件路径(如 feature.issue)", ), output_dir: Optional[Path] = typer.Option( None, "--output", "-o", help="项目根目录,默认为当前目录" ), api_key: Optional[str] = typer.Option( None, "--api-key", envvar="DEEPSEEK_APIKEY", help="API密钥" ), base_url: str = typer.Option( "https://api.deepseek.com", "--base-url", help="API基础URL" ), model: str = typer.Option("deepseek-reasoner", "--model", "-m", help="使用的模型"), log_file: Optional[str] = typer.Option(None, "--log", help="日志文件路径"), max_concurrency: int = typer.Option( 4, "--max-concurrency", help="并发生成的最大工作线程数,默认4" ), ): """增强项目:根据需求工单添加新功能。""" if output_dir is None: output_dir = Path.cwd() # 初始化日志配置 log_file_path = init_logging(output_dir, log_file, command_name="enhance") # 处理致命错误:检查design.json是否存在 design_path = output_dir / "design.json" if not design_path.exists(): logger.error( f"design.json 不存在于 {output_dir},请先运行 init 命令初始化项目。" ) raise typer.Exit(code=1) with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), console=console, ) as progress: task_id = progress.add_task("正在增强项目...", total=1) # 修改:设置总任务数为1以控制进度显示 generator = EnhanceGenerator( api_key=api_key, base_url=base_url, model=model, output_dir=str(output_dir), log_file=log_file_path, max_concurrency=max_concurrency, ) success = generator.process_enhance(issue_file, output_format="full") if success: progress.update(task_id, completed=1, description="增强处理完成") # 修改:成功时更新完成状态 else: progress.update(task_id, description="增强处理失败") # 修改:失败时更新描述 if not success: logger.error("增强处理失败") raise typer.Exit(code=1) console.print("[green]增强处理完成。成功处理文件,详情请查看日志。[/green]") @app.command() def fix( issue_file: Path = typer.Argument( ..., exists=True, file_okay=True, dir_okay=False, help="Bug工单文件路径(如 bug.issue)", ), output_dir: Optional[Path] = typer.Option( None, "--output", "-o", help="项目根目录,默认为当前目录" ), api_key: Optional[str] = typer.Option( None, "--api-key", envvar="DEEPSEEK_APIKEY", help="API密钥" ), base_url: str = typer.Option( "https://api.deepseek.com", "--base-url", help="API基础URL" ), model: str = typer.Option("deepseek-reasoner", "--model", "-m", help="使用的模型"), log_file: Optional[str] = typer.Option(None, "--log", help="日志文件路径"), max_concurrency: int = typer.Option( 4, "--max-concurrency", help="并发生成的最大工作线程数,默认4" ), ): """修复项目:根据Bug工单自动修复 Bug。""" if output_dir is None: output_dir = Path.cwd() # 初始化日志配置 log_file_path = init_logging(output_dir, log_file, command_name="fix") # 处理致命错误:检查design.json是否存在 design_path = output_dir / "design.json" if not design_path.exists(): logger.error(f"design.json 不存在于 {output_dir},请确保项目已初始化。") raise typer.Exit(code=1) try: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), console=console ) as progress: task_id = progress.add_task("正在修复项目...", total=1) # 修改:设置总任务数为1以控制进度显示 generator = FixGenerator( api_key=api_key, base_url=base_url, model=model, output_dir=str(output_dir), log_file=log_file_path, max_concurrency=max_concurrency, ) success = generator.process_fix(issue_file, output_format="full") if success: progress.update(task_id, completed=1, description="修复处理完成") # 修改:成功时更新完成状态 else: progress.update(task_id, description="修复处理失败") # 修改:失败时更新描述 if not success: logger.error("修复处理失败") raise typer.Exit(code=1) console.print("[green]修复处理完成。成功处理文件,详情请查看日志。[/green]") except Exception as e: logger.error(f"修复失败: {e}") raise typer.Exit(code=1) @app.command() def design( file: Optional[Path] = typer.Option( None, "--file", "-f", help="README文件路径,用于生成design.json;如果与--source同时使用,则生成design.json后从源代码刷新", exists=True, file_okay=True, dir_okay=False, ), source: Optional[Path] = typer.Option( None, "--source", help="源代码目录路径,用于从源代码刷新design.json;必须为目录", exists=True, file_okay=False, dir_okay=True, ), output_dir: Optional[Path] = typer.Option( None, "--output", "-o", help="输出目录,design.json将保存在此,默认为当前目录" ), force: bool = typer.Option(False, "--force", help="强制覆盖已存在的design.json,或强制从源代码刷新"), dry_run: bool = typer.Option(False, "--dry-run", help="模拟运行,不实际写入文件或执行命令,仅打印信息"), api_key: Optional[str] = typer.Option( None, "--api-key", envvar="DEEPSEEK_APIKEY", help="API密钥" ), base_url: str = typer.Option( "https://api.deepseek.com", "--base-url", help="API基础URL" ), model: str = typer.Option("deepseek-reasoner", "--model", "-m", help="使用的模型"), log_file: Optional[str] = typer.Option(None, "--log", help="日志文件路径"), max_concurrency: int = typer.Option( 4, "--max-concurrency", help="并发生成的最大工作线程数,默认4" ), ): """生成或更新design.json:支持从README生成、从源代码刷新,并集成新的设计生成逻辑。""" if output_dir is None: output_dir = Path.cwd() # 初始化日志配置 log_file_path = init_logging(output_dir, log_file, command_name="design") # 检查是否提供了至少一个操作参数 if file is None and source is None: logger.error("必须提供 --file 或 --source 参数之一来执行操作。") raise typer.Exit(code=1) # 初始化DesignGenerator以集成新的设计生成逻辑 try: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), console=console, ) as progress: task_id = progress.add_task("正在处理design命令...", total=1) generator = DesignGenerator( api_key=api_key, base_url=base_url, model=model, output_dir=str(output_dir), log_file=log_file_path, max_concurrency=max_concurrency, ) design_path = output_dir / "design.json" # 处理--dry-run选项 if dry_run: console.print("[yellow]模拟运行模式:不会实际写入文件或执行命令。[/yellow]") if file is not None: logger.info(f"模拟:将从README文件 {file} 生成design.json") # 在dry-run模式下,仅模拟解析README content = generator.parse_readme(file) console.print(f"[blue]模拟解析README内容完成,长度: {len(content)} 字符[/blue]") if source is not None: logger.info(f"模拟:将从源代码目录 {source} 刷新design.json") # 模拟分析源代码 design_info = generator.analyze_source_files(source) console.print(f"[blue]模拟分析完成,共分析 {len(design_info['files'])} 个文件[/blue]") progress.update(task_id, completed=1, description="模拟运行完成") console.print("[green]✅ 模拟运行完成,无实际文件操作。[/green]") return # 实际运行逻辑 if file is not None: # 生成design.json从README if not force and design_path.exists(): logger.error( f"design.json 已存在于 {design_path}。使用 --force 参数以强制覆盖。" ) raise typer.Exit(code=1) generator.run(readme_path=file) logger.info(f"已从README生成design.json: {design_path}") if source is not None: # 从源代码刷新design.json success = generator.refresh_design_from_source(source) if not success: logger.error("从源代码刷新design.json失败") raise typer.Exit(code=1) logger.info(f"已从源代码刷新design.json: {design_path}") progress.update(task_id, completed=1, description="design命令处理完成") console.print(f"[green]✅ design.json 已处理完成,路径: {design_path}[/green]") except Exception as e: logger.error(f"处理design命令失败: {e}") raise typer.Exit(code=1) if __name__ == "__main__": app()