diff --git a/.gitignore b/.gitignore index a75ca5d..97a514f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,31 @@ test_output/ *.egg-info */*.egg-info /logs/* -/llm_responses/* \ No newline at end of file +/llm_responses/* +/.vscode/* +/.idea/* +/.vs/* +/.vscode-server/* +/.env +/.env.local +/.env.development.local +/.env.test.local +/.env.production.local +/.env.development +/.env.test +/.env.production +/.env.example +/.env.template +/.env.default +/.env.defaults +/.env.defaults.local +/.env.defaults.development +/.env.defaults.test +/.env.defaults.production +/.env.defaults.development.local +/.env.defaults.test.local +/.env.defaults.production.local +/.env.defaults.development.example +/.env.defaults.test.example +/.env.defaults.production.example +/issues/* \ No newline at end of file diff --git a/README.md b/README.md index 778c4c1..ea5dae8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LLM 代码生成工具(自举版 · 增强版) +# LLM 代码生成工具 本项目是一个基于大语言模型的智能代码生成与维护工具。它不仅能够根据项目 `README.md` 描述**自动生成完整的 Python 包代码**,还支持**在现有项目上增量添加功能**和**自动修复 Bug**。工具采用 `uv` 管理依赖,包含单元测试、并行检查、断点续写等特性,并通过一个**面向 LLM 的中间设计层**来提升生成质量和可维护性。 @@ -146,7 +146,7 @@ llm-codegen design project_readme.md -o ./my_design ```json { "project_name": "MyProject", - "version": "1.0.0", + "version": "X.X.X", "description": "项目简短描述", "files": [ { @@ -174,6 +174,31 @@ llm-codegen design project_readme.md -o ./my_design 该文件由 LLM 在 `init` 阶段生成,并在后续所有操作中作为上下文提供给 LLM,确保每次生成都符合整体设计。 +## 📄 README 作为设计资产 + +README.md 不仅是项目文档,更是工具的关键设计资产。它作为项目的起点,用于生成中间设计文件 `design.json`,并在此后的所有操作中提供上下文。 + +### 重要性 + +- **设计输入**: README.md 描述了项目的整体目标和功能,是 LLM 生成代码和设计的基础。它确保了生成的代码与项目意图保持一致。 +- **一致性保障**: 通过保持 README.md 和 `design.json` 同步,工具可以基于准确的设计信息进行操作,避免生成偏差,提升维护效率。 + +### 同步机制 + +工具提供了自动同步机制来维护 README.md 和 `design.json` 的一致性: + +- **`sync_readme` 方法**: 在 `CodeGenerator` 类中(位于 `src/llm_codegen/core.py`),`sync_readme` 方法可以比较 README.md 的内容和 `design.json` 中的描述,自动更新 `design.json` 以反映 README.md 的变更,或报告差异以供审查。 +- **集成到工作流**: 同步机制可以集成到 `enhance` 或 `fix` 操作中,在生成代码前后自动运行,确保设计信息始终最新。 + +### 如何保持与 design.json 一致 + +为了确保 README.md 和 `design.json` 保持同步,建议: + +1. **定期同步**: 在修改 README.md 后,运行 `llm-codegen` 工具的相关命令(如 `enhance` 或通过自定义脚本调用 `sync_readme`)来更新 `design.json`。 +2. **避免手动修改**: 尽量不要手动编辑 `design.json`,除非完全理解设计意图。工具生成的 `design.json` 应作为权威来源,手动修改可能导致后续操作错误。 +3. **检查不一致**: 工具在运行 `enhance` 或 `fix` 时,可以自动检查 README.md 和 `design.json` 之间的不一致,并提示用户进行同步,防止设计漂移。 + +通过维护这种同步,项目可以持续演进,而设计资产始终保持最新和准确,确保 LLM 生成的代码始终符合项目设计。 ## 🔄 核心工作流 diff --git a/check_results.json b/check_results.json deleted file mode 100644 index 6cad94f..0000000 --- a/check_results.json +++ /dev/null @@ -1,122 +0,0 @@ -[ - { - "tool": "black", - "file": "tests/__init__.py", - "returncode": 0, - "stdout": "", - "stderr": "All done! ✨ 🍰 ✨\n1 file would be left unchanged.\n", - "errors": [ - "All done! ✨ 🍰 ✨\n1 file would be left unchanged." - ] - }, - { - "tool": "black", - "file": "tests/test_cli.py", - "returncode": 1, - "stdout": "--- tests/test_cli.py\t2026-03-17 14:27:38.333874+00:00\n+++ tests/test_cli.py\t2026-03-17 14:57:42.893165+00:00\n@@ -10,143 +10,153 @@\n \n \n def test_cli_init_success():\n \"\"\"测试 init 命令成功执行\"\"\"\n from src.llm_codegen.cli import app # 假设从项目根目录运行测试\n- \n+\n # 模拟 CodeGenerator 和其方法,避免实际调用 API\n- with patch('src.llm_codegen.cli.CodeGenerator') as mock_generator:\n+ with patch(\"src.llm_codegen.cli.CodeGenerator\") as mock_generator:\n mock_instance = Mock()\n mock_instance.run = Mock()\n mock_generator.return_value = mock_instance\n- \n+\n # 创建一个虚拟的 README 文件用于测试\n test_readme = Path(\"test_readme.md\")\n test_readme.write_text(\"# Test Project\\n\\nA test project for CLI.\")\n- \n- result = runner.invoke(app, [\"init\", str(test_readme), \"--output\", \"./test_output\"])\n- \n+\n+ result = runner.invoke(\n+ app, [\"init\", str(test_readme), \"--output\", \"./test_output\"]\n+ )\n+\n # 清理\n test_readme.unlink()\n- \n+\n assert result.exit_code == 0\n assert \"初始化失败\" not in result.stdout\n mock_generator.assert_called_once()\n mock_instance.run.assert_called_once_with(test_readme)\n \n \n def test_cli_init_failure_no_readme():\n \"\"\"测试 init 命令当 README 不存在时失败\"\"\"\n from src.llm_codegen.cli import app\n- \n+\n result = runner.invoke(app, [\"init\", \"nonexistent.md\"])\n- \n+\n assert result.exit_code != 0 # 应该退出码非零\n \n \n def test_cli_enhance_success():\n \"\"\"测试 enhance 命令成功执行(简化版,基于工单)\"\"\"\n from src.llm_codegen.cli import app\n- \n+\n # 模拟依赖文件和环境\n- with patch('src.llm_codegen.cli.CodeGenerator') as mock_generator, \\\n- patch('src.llm_codegen.cli.Checker') as mock_checker, \\\n- patch('pathlib.Path.exists') as mock_exists:\n- \n+ with (\n+ patch(\"src.llm_codegen.cli.CodeGenerator\") as mock_generator,\n+ patch(\"src.llm_codegen.cli.Checker\") as mock_checker,\n+ patch(\"pathlib.Path.exists\") as mock_exists,\n+ ):\n+\n mock_exists.return_value = True # 模拟 design.json 存在\n mock_instance = Mock()\n mock_instance.run_full_check_and_fix = Mock(return_value=True)\n mock_checker.return_value = mock_instance\n mock_generator.return_value = Mock()\n- \n+\n # 创建一个虚拟的工单文件\n test_issue = Path(\"test_feature.issue\")\n test_issue.write_text(\"name: Add feature\\ndescription: Test feature\")\n- \n- result = runner.invoke(app, [\"enhance\", str(test_issue), \"--output\", \"./test_output\"])\n- \n+\n+ result = runner.invoke(\n+ app, [\"enhance\", str(test_issue), \"--output\", \"./test_output\"]\n+ )\n+\n # 清理\n test_issue.unlink()\n- \n+\n assert result.exit_code == 0\n assert \"增强失败\" not in result.stdout\n mock_checker.assert_called_once()\n mock_instance.run_full_check_and_fix.assert_called_once()\n \n \n def test_cli_fix_success():\n \"\"\"测试 fix 命令成功执行(简化版,基于工单)\"\"\"\n from src.llm_codegen.cli import app\n- \n- with patch('src.llm_codegen.cli.CodeGenerator') as mock_generator, \\\n- patch('src.llm_codegen.cli.Checker') as mock_checker, \\\n- patch('pathlib.Path.exists') as mock_exists:\n- \n+\n+ with (\n+ patch(\"src.llm_codegen.cli.CodeGenerator\") as mock_generator,\n+ patch(\"src.llm_codegen.cli.Checker\") as mock_checker,\n+ patch(\"pathlib.Path.exists\") as mock_exists,\n+ ):\n+\n mock_exists.return_value = True\n mock_instance = Mock()\n mock_instance.run_full_check_and_fix = Mock(return_value=True)\n mock_checker.return_value = mock_instance\n mock_generator.return_value = Mock()\n- \n+\n test_issue = Path(\"test_bug.issue\")\n test_issue.write_text(\"name: Fix bug\\ndescription: Test bug\")\n- \n- result = runner.invoke(app, [\"fix\", str(test_issue), \"--output\", \"./test_output\"])\n- \n+\n+ result = runner.invoke(\n+ app, [\"fix\", str(test_issue), \"--output\", \"./test_output\"]\n+ )\n+\n test_issue.unlink()\n- \n+\n assert result.exit_code == 0\n assert \"修复失败\" not in result.stdout\n mock_checker.assert_called_once()\n mock_instance.run_full_check_and_fix.assert_called_once()\n \n \n def test_cli_help():\n \"\"\"测试 CLI 帮助命令\"\"\"\n from src.llm_codegen.cli import app\n- \n+\n result = runner.invoke(app, [\"--help\"])\n assert result.exit_code == 0\n assert \"基于LLM的自动化代码生成与维护工具\" in result.stdout\n- \n+\n # 测试子命令帮助\n result = runner.invoke(app, [\"init\", \"--help\"])\n assert result.exit_code == 0\n assert \"README.md 文件路径\" in result.stdout\n \n \n def test_cli_enhance_no_design():\n \"\"\"测试 enhance 命令当 design.json 不存在时失败\"\"\"\n from src.llm_codegen.cli import app\n- \n- with patch('pathlib.Path.exists') as mock_exists:\n+\n+ with patch(\"pathlib.Path.exists\") as mock_exists:\n mock_exists.return_value = False # 模拟 design.json 不存在\n- \n+\n test_issue = Path(\"test_feature.issue\")\n test_issue.write_text(\"name: Test\")\n- \n+\n result = runner.invoke(app, [\"enhance\", str(test_issue)])\n- \n+\n test_issue.unlink()\n- \n+\n assert result.exit_code != 0\n \n \n def test_cli_fix_no_design():\n \"\"\"测试 fix 命令当 design.json 不存在时失败\"\"\"\n from src.llm_codegen.cli import app\n- \n- with patch('pathlib.Path.exists') as mock_exists:\n+\n+ with patch(\"pathlib.Path.exists\") as mock_exists:\n mock_exists.return_value = False\n- \n+\n test_issue = Path(\"test_bug.issue\")\n test_issue.write_text(\"name: Test\")\n- \n+\n result = runner.invoke(app, [\"fix\", str(test_issue)])\n- \n+\n test_issue.unlink()\n- \n+\n assert result.exit_code != 0\n \n \n if __name__ == \"__main__\":\n pytest.main([__file__])\n", - "stderr": "would reformat tests/test_cli.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat tests/test_cli.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- tests/test_cli.py\t2026-03-17 14:27:38.333874+00:00\n+++ tests/test_cli.py\t2026-03-17 14:57:42.893165+00:00\n@@ -10,143 +10,153 @@\n \n \n def test_cli_init_success():\n \"\"\"测试 init 命令成功执行\"\"\"\n from src.llm_codegen.cli import app # 假设从项目根目录运行测试\n- \n+\n # 模拟 CodeGenerator 和其方法,避免实际调用 API\n- with patch('src.llm_codegen.cli.CodeGenerator') as mock_generator:\n+ with patch(\"src.llm_codegen.cli.CodeGenerator\") as mock_generator:\n mock_instance = Mock()\n mock_instance.run = Mock()\n mock_generator.return_value = mock_instance\n- \n+\n # 创建一个虚拟的 README 文件用于测试\n test_readme = Path(\"test_readme.md\")\n test_readme.write_text(\"# Test Project\\n\\nA test project for CLI.\")\n- \n- result = runner.invoke(app, [\"init\", str(test_readme), \"--output\", \"./test_output\"])\n- \n+\n+ result = runner.invoke(\n+ app, [\"init\", str(test_readme), \"--output\", \"./test_output\"]\n+ )\n+\n # 清理\n test_readme.unlink()\n- \n+\n assert result.exit_code == 0\n assert \"初始化失败\" not in result.stdout\n mock_generator.assert_called_once()\n mock_instance.run.assert_called_once_with(test_readme)\n \n \n def test_cli_init_failure_no_readme():\n \"\"\"测试 init 命令当 README 不存在时失败\"\"\"\n from src.llm_codegen.cli import app\n- \n+\n result = runner.invoke(app, [\"init\", \"nonexistent.md\"])\n- \n+\n assert result.exit_code != 0 # 应该退出码非零\n \n \n def test_cli_enhance_success():\n \"\"\"测试 enhance 命令成功执行(简化版,基于工单)\"\"\"\n from src.llm_codegen.cli import app\n- \n+\n # 模拟依赖文件和环境\n- with patch('src.llm_codegen.cli.CodeGenerator') as mock_generator, \\\n- patch('src.llm_codegen.cli.Checker') as mock_checker, \\\n- patch('pathlib.Path.exists') as mock_exists:\n- \n+ with (\n+ patch(\"src.llm_codegen.cli.CodeGenerator\") as mock_generator,\n+ patch(\"src.llm_codegen.cli.Checker\") as mock_checker,\n+ patch(\"pathlib.Path.exists\") as mock_exists,\n+ ):\n+\n mock_exists.return_value = True # 模拟 design.json 存在\n mock_instance = Mock()\n mock_instance.run_full_check_and_fix = Mock(return_value=True)\n mock_checker.return_value = mock_instance\n mock_generator.return_value = Mock()\n- \n+\n # 创建一个虚拟的工单文件\n test_issue = Path(\"test_feature.issue\")\n test_issue.write_text(\"name: Add feature\\ndescription: Test feature\")\n- \n- result = runner.invoke(app, [\"enhance\", str(test_issue), \"--output\", \"./test_output\"])\n- \n+\n+ result = runner.invoke(\n+ app, [\"enhance\", str(test_issue), \"--output\", \"./test_output\"]\n+ )\n+\n # 清理\n test_issue.unlink()\n- \n+\n assert result.exit_code == 0\n assert \"增强失败\" not in result.stdout\n mock_checker.assert_called_once()\n mock_instance.run_full_check_and_fix.assert_called_once()\n \n \n def test_cli_fix_success():\n \"\"\"测试 fix 命令成功执行(简化版,基于工单)\"\"\"\n from src.llm_codegen.cli import app\n- \n- with patch('src.llm_codegen.cli.CodeGenerator') as mock_generator, \\\n- patch('src.llm_codegen.cli.Checker') as mock_checker, \\\n- patch('pathlib.Path.exists') as mock_exists:\n- \n+\n+ with (\n+ patch(\"src.llm_codegen.cli.CodeGenerator\") as mock_generator,\n+ patch(\"src.llm_codegen.cli.Checker\") as mock_checker,\n+ patch(\"pathlib.Path.exists\") as mock_exists,\n+ ):\n+\n mock_exists.return_value = True\n mock_instance = Mock()\n mock_instance.run_full_check_and_fix = Mock(return_value=True)\n mock_checker.return_value = mock_instance\n mock_generator.return_value = Mock()\n- \n+\n test_issue = Path(\"test_bug.issue\")\n test_issue.write_text(\"name: Fix bug\\ndescription: Test bug\")\n- \n- result = runner.invoke(app, [\"fix\", str(test_issue), \"--output\", \"./test_output\"])\n- \n+\n+ result = runner.invoke(\n+ app, [\"fix\", str(test_issue), \"--output\", \"./test_output\"]\n+ )\n+\n test_issue.unlink()\n- \n+\n assert result.exit_code == 0\n assert \"修复失败\" not in result.stdout\n mock_checker.assert_called_once()\n mock_instance.run_full_check_and_fix.assert_called_once()\n \n \n def test_cli_help():\n \"\"\"测试 CLI 帮助命令\"\"\"\n from src.llm_codegen.cli import app\n- \n+\n result = runner.invoke(app, [\"--help\"])\n assert result.exit_code == 0\n assert \"基于LLM的自动化代码生成与维护工具\" in result.stdout\n- \n+\n # 测试子命令帮助\n result = runner.invoke(app, [\"init\", \"--help\"])\n assert result.exit_code == 0\n assert \"README.md 文件路径\" in result.stdout\n \n \n def test_cli_enhance_no_design():\n \"\"\"测试 enhance 命令当 design.json 不存在时失败\"\"\"\n from src.llm_codegen.cli import app\n- \n- with patch('pathlib.Path.exists') as mock_exists:\n+\n+ with patch(\"pathlib.Path.exists\") as mock_exists:\n mock_exists.return_value = False # 模拟 design.json 不存在\n- \n+\n test_issue = Path(\"test_feature.issue\")\n test_issue.write_text(\"name: Test\")\n- \n+\n result = runner.invoke(app, [\"enhance\", str(test_issue)])\n- \n+\n test_issue.unlink()\n- \n+\n assert result.exit_code != 0\n \n \n def test_cli_fix_no_design():\n \"\"\"测试 fix 命令当 design.json 不存在时失败\"\"\"\n from src.llm_codegen.cli import app\n- \n- with patch('pathlib.Path.exists') as mock_exists:\n+\n+ with patch(\"pathlib.Path.exists\") as mock_exists:\n mock_exists.return_value = False\n- \n+\n test_issue = Path(\"test_bug.issue\")\n test_issue.write_text(\"name: Test\")\n- \n+\n result = runner.invoke(app, [\"fix\", str(test_issue)])\n- \n+\n test_issue.unlink()\n- \n+\n assert result.exit_code != 0\n \n \n if __name__ == \"__main__\":\n pytest.main([__file__])" - ] - }, - { - "tool": "black", - "file": "tests/test_core.py", - "returncode": 1, - "stdout": "--- tests/test_core.py\t2026-03-17 14:27:38.333874+00:00\n+++ tests/test_core.py\t2026-03-17 14:57:42.956752+00:00\n@@ -9,31 +9,37 @@\n \n \n # ---------- Fake 类 ----------\n class FakeChatCompletion:\n \"\"\"模拟 OpenAI 的 chat.completions.create 返回值\"\"\"\n+\n def __init__(self, content):\n self.choices = [FakeChoice(FakeMessage(content))]\n+\n \n class FakeChoice:\n def __init__(self, message):\n self.message = message\n+\n \n class FakeMessage:\n def __init__(self, content):\n self.content = content\n self.reasoning_content = None\n \n \n class FakeOpenAIClient:\n \"\"\"假的 OpenAI 客户端,用于替换真实客户端\"\"\"\n+\n def __init__(self):\n self.chat = FakeChat()\n+\n \n class FakeChat:\n def __init__(self):\n self.completions = FakeCompletions()\n+\n \n class FakeCompletions:\n def __init__(self):\n self.create_called = False\n self.create_kwargs = None\n@@ -50,11 +56,13 @@\n # ---------- Fixtures ----------\n @pytest.fixture\n def fake_openai_client(monkeypatch):\n \"\"\"用假的 OpenAI 客户端替换真实的客户端\"\"\"\n fake_client = FakeOpenAIClient()\n- monkeypatch.setattr(\"src.llm_codegen.core.OpenAI\", lambda *args, **kwargs: fake_client)\n+ monkeypatch.setattr(\n+ \"src.llm_codegen.core.OpenAI\", lambda *args, **kwargs: fake_client\n+ )\n return fake_client\n \n \n @pytest.fixture\n def code_generator(tmp_path, monkeypatch, fake_openai_client):\n@@ -103,14 +111,16 @@\n \"project_name\": \"test-project\",\n \"version\": \"1.0.0\",\n \"description\": \"A test project\",\n \"files\": [],\n \"commands\": [],\n- \"check_tools\": []\n+ \"check_tools\": [],\n }\n \n- def fake_call_llm(system_prompt, user_prompt, temperature=0.2, expect_json=True):\n+ def fake_call_llm(\n+ system_prompt, user_prompt, temperature=0.2, expect_json=True\n+ ):\n return mock_response\n \n monkeypatch.setattr(code_generator, \"_call_llm\", fake_call_llm)\n \n design = code_generator.generate_design_json()\n@@ -122,11 +132,13 @@\n assert design_path.exists()\n with open(design_path) as f:\n saved = json.load(f)\n assert saved[\"project_name\"] == \"test-project\"\n \n- def test_generate_file_with_dependencies(self, code_generator, monkeypatch, tmp_path):\n+ def test_generate_file_with_dependencies(\n+ self, code_generator, monkeypatch, tmp_path\n+ ):\n \"\"\"测试生成文件,有依赖文件\"\"\"\n # 创建依赖文件\n dep_path = tmp_path / \"dep.py\"\n dep_path.write_text(\"# Dependency file\")\n code_generator.output_dir = tmp_path\n@@ -134,50 +146,62 @@\n \n # 模拟 _call_llm 的返回值\n llm_response = {\n \"code\": \"print('Hello, world!')\",\n \"description\": \"测试文件\",\n- \"commands\": []\n+ \"commands\": [],\n }\n \n- def fake_call_llm(system_prompt, user_prompt, temperature=0.2, expect_json=True):\n+ def fake_call_llm(\n+ system_prompt, user_prompt, temperature=0.2, expect_json=True\n+ ):\n return llm_response\n \n monkeypatch.setattr(code_generator, \"_call_llm\", fake_call_llm)\n \n code, desc, commands = code_generator.generate_file(\n file_path=\"test.py\",\n prompt_instruction=\"生成测试文件\",\n- dependency_files=[str(dep_path)]\n+ dependency_files=[str(dep_path)],\n )\n \n assert code == \"print('Hello, world!')\"\n assert desc == \"测试文件\"\n assert commands == []\n \n def test_execute_command_success(self, code_generator, monkeypatch):\n \"\"\"测试执行命令成功\"\"\"\n+\n def fake_run(cmd, *args, **kwargs):\n- return subprocess.CompletedProcess(args=cmd, returncode=0, stdout=\"\", stderr=\"\")\n+ return subprocess.CompletedProcess(\n+ args=cmd, returncode=0, stdout=\"\", stderr=\"\"\n+ )\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run)\n \n success = code_generator.execute_command(\"echo test\")\n assert success is True\n \n def test_execute_command_dangerous(self, code_generator, monkeypatch):\n \"\"\"测试阻止危险命令\"\"\"\n+\n def fake_dangerous(cmd):\n return (True, \"包含危险关键词\")\n+\n monkeypatch.setattr(\"src.llm_codegen.core.is_dangerous_command\", fake_dangerous)\n \n success = code_generator.execute_command(\"rm -rf /\")\n assert success is False\n \n def test_execute_command_failure(self, code_generator, monkeypatch):\n \"\"\"测试命令执行失败\"\"\"\n+\n def fake_run(cmd, *args, **kwargs):\n- return subprocess.CompletedProcess(args=cmd, returncode=1, stdout=\"\", stderr=\"\")\n+ return subprocess.CompletedProcess(\n+ args=cmd, returncode=1, stdout=\"\", stderr=\"\"\n+ )\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run)\n \n success = code_generator.execute_command(\"false\")\n assert success is False\n \n@@ -189,44 +213,65 @@\n \"current_file_index\": 1,\n \"generated_files\": [\"file1.py\"],\n \"dependencies_map\": {},\n \"total_files\": 3,\n \"output_dir\": str(tmp_path),\n- \"readme_path\": \"test\"\n+ \"readme_path\": \"test\",\n }\n state_file.write_text(json.dumps(state_data))\n \n # 创建设计文件\n design_path = tmp_path / \"design.json\"\n design_data = {\n \"project_name\": \"test\",\n \"version\": \"1.0.0\",\n \"description\": \"test\",\n \"files\": [\n- {\"path\": \"file1.py\", \"summary\": \"\", \"dependencies\": [], \"functions\": [], \"classes\": []},\n- {\"path\": \"file2.py\", \"summary\": \"\", \"dependencies\": [], \"functions\": [], \"classes\": []},\n- {\"path\": \"file3.py\", \"summary\": \"\", \"dependencies\": [], \"functions\": [], \"classes\": []}\n+ {\n+ \"path\": \"file1.py\",\n+ \"summary\": \"\",\n+ \"dependencies\": [],\n+ \"functions\": [],\n+ \"classes\": [],\n+ },\n+ {\n+ \"path\": \"file2.py\",\n+ \"summary\": \"\",\n+ \"dependencies\": [],\n+ \"functions\": [],\n+ \"classes\": [],\n+ },\n+ {\n+ \"path\": \"file3.py\",\n+ \"summary\": \"\",\n+ \"dependencies\": [],\n+ \"functions\": [],\n+ \"classes\": [],\n+ },\n ],\n \"commands\": [],\n- \"check_tools\": []\n+ \"check_tools\": [],\n }\n design_path.write_text(json.dumps(design_data))\n \n code_generator.output_dir = tmp_path\n code_generator.state_file = state_file\n \n # 模拟内部方法\n def fake_parse_readme(path):\n return \"# README\"\n+\n monkeypatch.setattr(code_generator, \"parse_readme\", fake_parse_readme)\n \n def fake_generate_file(file_path, prompt_instruction, dependency_files):\n return (\"code\", \"desc\", [])\n+\n monkeypatch.setattr(code_generator, \"generate_file\", fake_generate_file)\n \n def fake_execute_command(cmd, cwd=None):\n return True\n+\n monkeypatch.setattr(code_generator, \"execute_command\", fake_execute_command)\n \n # 运行,预期不抛出异常\n code_generator.run(Path(tmp_path / \"README.md\"))\n \n@@ -238,27 +283,35 @@\n code_generator.output_dir = tmp_path\n \n # 模拟 parse_readme\n def fake_parse_readme(path):\n return \"# README\"\n+\n monkeypatch.setattr(code_generator, \"parse_readme\", fake_parse_readme)\n \n # 模拟 generate_design_json 返回设计\n fake_design = DesignModel(\n project_name=\"test\",\n version=\"1.0.0\",\n description=\"test\",\n files=[], # 无文件,简化流程\n commands=[],\n- check_tools=[]\n+ check_tools=[],\n )\n+\n def fake_generate_design_json():\n return fake_design\n- monkeypatch.setattr(code_generator, \"generate_design_json\", fake_generate_design_json)\n+\n+ monkeypatch.setattr(\n+ code_generator, \"generate_design_json\", fake_generate_design_json\n+ )\n \n # 模拟 get_project_structure\n def fake_get_project_structure():\n return [], {}\n- monkeypatch.setattr(code_generator, \"get_project_structure\", fake_get_project_structure)\n+\n+ monkeypatch.setattr(\n+ code_generator, \"get_project_structure\", fake_get_project_structure\n+ )\n \n # 运行,预期不抛出异常\n code_generator.run(Path(tmp_path / \"README.md\"))\n", - "stderr": "would reformat tests/test_core.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat tests/test_core.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- tests/test_core.py\t2026-03-17 14:27:38.333874+00:00\n+++ tests/test_core.py\t2026-03-17 14:57:42.956752+00:00\n@@ -9,31 +9,37 @@\n \n \n # ---------- Fake 类 ----------\n class FakeChatCompletion:\n \"\"\"模拟 OpenAI 的 chat.completions.create 返回值\"\"\"\n+\n def __init__(self, content):\n self.choices = [FakeChoice(FakeMessage(content))]\n+\n \n class FakeChoice:\n def __init__(self, message):\n self.message = message\n+\n \n class FakeMessage:\n def __init__(self, content):\n self.content = content\n self.reasoning_content = None\n \n \n class FakeOpenAIClient:\n \"\"\"假的 OpenAI 客户端,用于替换真实客户端\"\"\"\n+\n def __init__(self):\n self.chat = FakeChat()\n+\n \n class FakeChat:\n def __init__(self):\n self.completions = FakeCompletions()\n+\n \n class FakeCompletions:\n def __init__(self):\n self.create_called = False\n self.create_kwargs = None\n@@ -50,11 +56,13 @@\n # ---------- Fixtures ----------\n @pytest.fixture\n def fake_openai_client(monkeypatch):\n \"\"\"用假的 OpenAI 客户端替换真实的客户端\"\"\"\n fake_client = FakeOpenAIClient()\n- monkeypatch.setattr(\"src.llm_codegen.core.OpenAI\", lambda *args, **kwargs: fake_client)\n+ monkeypatch.setattr(\n+ \"src.llm_codegen.core.OpenAI\", lambda *args, **kwargs: fake_client\n+ )\n return fake_client\n \n \n @pytest.fixture\n def code_generator(tmp_path, monkeypatch, fake_openai_client):\n@@ -103,14 +111,16 @@\n \"project_name\": \"test-project\",\n \"version\": \"1.0.0\",\n \"description\": \"A test project\",\n \"files\": [],\n \"commands\": [],\n- \"check_tools\": []\n+ \"check_tools\": [],\n }\n \n- def fake_call_llm(system_prompt, user_prompt, temperature=0.2, expect_json=True):\n+ def fake_call_llm(\n+ system_prompt, user_prompt, temperature=0.2, expect_json=True\n+ ):\n return mock_response\n \n monkeypatch.setattr(code_generator, \"_call_llm\", fake_call_llm)\n \n design = code_generator.generate_design_json()\n@@ -122,11 +132,13 @@\n assert design_path.exists()\n with open(design_path) as f:\n saved = json.load(f)\n assert saved[\"project_name\"] == \"test-project\"\n \n- def test_generate_file_with_dependencies(self, code_generator, monkeypatch, tmp_path):\n+ def test_generate_file_with_dependencies(\n+ self, code_generator, monkeypatch, tmp_path\n+ ):\n \"\"\"测试生成文件,有依赖文件\"\"\"\n # 创建依赖文件\n dep_path = tmp_path / \"dep.py\"\n dep_path.write_text(\"# Dependency file\")\n code_generator.output_dir = tmp_path\n@@ -134,50 +146,62 @@\n \n # 模拟 _call_llm 的返回值\n llm_response = {\n \"code\": \"print('Hello, world!')\",\n \"description\": \"测试文件\",\n- \"commands\": []\n+ \"commands\": [],\n }\n \n- def fake_call_llm(system_prompt, user_prompt, temperature=0.2, expect_json=True):\n+ def fake_call_llm(\n+ system_prompt, user_prompt, temperature=0.2, expect_json=True\n+ ):\n return llm_response\n \n monkeypatch.setattr(code_generator, \"_call_llm\", fake_call_llm)\n \n code, desc, commands = code_generator.generate_file(\n file_path=\"test.py\",\n prompt_instruction=\"生成测试文件\",\n- dependency_files=[str(dep_path)]\n+ dependency_files=[str(dep_path)],\n )\n \n assert code == \"print('Hello, world!')\"\n assert desc == \"测试文件\"\n assert commands == []\n \n def test_execute_command_success(self, code_generator, monkeypatch):\n \"\"\"测试执行命令成功\"\"\"\n+\n def fake_run(cmd, *args, **kwargs):\n- return subprocess.CompletedProcess(args=cmd, returncode=0, stdout=\"\", stderr=\"\")\n+ return subprocess.CompletedProcess(\n+ args=cmd, returncode=0, stdout=\"\", stderr=\"\"\n+ )\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run)\n \n success = code_generator.execute_command(\"echo test\")\n assert success is True\n \n def test_execute_command_dangerous(self, code_generator, monkeypatch):\n \"\"\"测试阻止危险命令\"\"\"\n+\n def fake_dangerous(cmd):\n return (True, \"包含危险关键词\")\n+\n monkeypatch.setattr(\"src.llm_codegen.core.is_dangerous_command\", fake_dangerous)\n \n success = code_generator.execute_command(\"rm -rf /\")\n assert success is False\n \n def test_execute_command_failure(self, code_generator, monkeypatch):\n \"\"\"测试命令执行失败\"\"\"\n+\n def fake_run(cmd, *args, **kwargs):\n- return subprocess.CompletedProcess(args=cmd, returncode=1, stdout=\"\", stderr=\"\")\n+ return subprocess.CompletedProcess(\n+ args=cmd, returncode=1, stdout=\"\", stderr=\"\"\n+ )\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run)\n \n success = code_generator.execute_command(\"false\")\n assert success is False\n \n@@ -189,44 +213,65 @@\n \"current_file_index\": 1,\n \"generated_files\": [\"file1.py\"],\n \"dependencies_map\": {},\n \"total_files\": 3,\n \"output_dir\": str(tmp_path),\n- \"readme_path\": \"test\"\n+ \"readme_path\": \"test\",\n }\n state_file.write_text(json.dumps(state_data))\n \n # 创建设计文件\n design_path = tmp_path / \"design.json\"\n design_data = {\n \"project_name\": \"test\",\n \"version\": \"1.0.0\",\n \"description\": \"test\",\n \"files\": [\n- {\"path\": \"file1.py\", \"summary\": \"\", \"dependencies\": [], \"functions\": [], \"classes\": []},\n- {\"path\": \"file2.py\", \"summary\": \"\", \"dependencies\": [], \"functions\": [], \"classes\": []},\n- {\"path\": \"file3.py\", \"summary\": \"\", \"dependencies\": [], \"functions\": [], \"classes\": []}\n+ {\n+ \"path\": \"file1.py\",\n+ \"summary\": \"\",\n+ \"dependencies\": [],\n+ \"functions\": [],\n+ \"classes\": [],\n+ },\n+ {\n+ \"path\": \"file2.py\",\n+ \"summary\": \"\",\n+ \"dependencies\": [],\n+ \"functions\": [],\n+ \"classes\": [],\n+ },\n+ {\n+ \"path\": \"file3.py\",\n+ \"summary\": \"\",\n+ \"dependencies\": [],\n+ \"functions\": [],\n+ \"classes\": [],\n+ },\n ],\n \"commands\": [],\n- \"check_tools\": []\n+ \"check_tools\": [],\n }\n design_path.write_text(json.dumps(design_data))\n \n code_generator.output_dir = tmp_path\n code_generator.state_file = state_file\n \n # 模拟内部方法\n def fake_parse_readme(path):\n return \"# README\"\n+\n monkeypatch.setattr(code_generator, \"parse_readme\", fake_parse_readme)\n \n def fake_generate_file(file_path, prompt_instruction, dependency_files):\n return (\"code\", \"desc\", [])\n+\n monkeypatch.setattr(code_generator, \"generate_file\", fake_generate_file)\n \n def fake_execute_command(cmd, cwd=None):\n return True\n+\n monkeypatch.setattr(code_generator, \"execute_command\", fake_execute_command)\n \n # 运行,预期不抛出异常\n code_generator.run(Path(tmp_path / \"README.md\"))\n \n@@ -238,27 +283,35 @@\n code_generator.output_dir = tmp_path\n \n # 模拟 parse_readme\n def fake_parse_readme(path):\n return \"# README\"\n+\n monkeypatch.setattr(code_generator, \"parse_readme\", fake_parse_readme)\n \n # 模拟 generate_design_json 返回设计\n fake_design = DesignModel(\n project_name=\"test\",\n version=\"1.0.0\",\n description=\"test\",\n files=[], # 无文件,简化流程\n commands=[],\n- check_tools=[]\n+ check_tools=[],\n )\n+\n def fake_generate_design_json():\n return fake_design\n- monkeypatch.setattr(code_generator, \"generate_design_json\", fake_generate_design_json)\n+\n+ monkeypatch.setattr(\n+ code_generator, \"generate_design_json\", fake_generate_design_json\n+ )\n \n # 模拟 get_project_structure\n def fake_get_project_structure():\n return [], {}\n- monkeypatch.setattr(code_generator, \"get_project_structure\", fake_get_project_structure)\n+\n+ monkeypatch.setattr(\n+ code_generator, \"get_project_structure\", fake_get_project_structure\n+ )\n \n # 运行,预期不抛出异常\n code_generator.run(Path(tmp_path / \"README.md\"))" - ] - }, - { - "tool": "black", - "file": "llmcodegen.py", - "returncode": 1, - "stdout": "--- llmcodegen.py\t2026-03-17 14:27:38.333874+00:00\n+++ llmcodegen.py\t2026-03-17 14:57:43.010113+00:00\n@@ -22,10 +22,11 @@\n DANGEROUS_COMMANDS = [\"rm\", \"sudo\", \"chmod\", \"dd\", \"mkfs\", \"> /dev/sda\", \"format\"]\n ALLOWED_COMMANDS = [] # 可设置白名单,为空则只检查黑名单\n \n app = typer.Typer(help=\"基于LLM的自动化代码生成工具\")\n console = Console()\n+\n \n # ==================== 工具函数 ====================\n def is_dangerous_command(cmd: str) -> Tuple[bool, str]:\n \"\"\"\n 判断命令是否危险\n@@ -35,10 +36,11 @@\n for danger in DANGEROUS_COMMANDS:\n if danger in cmd_lower:\n return True, f\"包含危险关键词 '{danger}'\"\n return False, \"\"\n \n+\n # ==================== 核心类 ====================\n class CodeGenerator:\n \"\"\"代码生成器,封装所有逻辑\"\"\"\n \n def __init__(\n@@ -136,13 +138,15 @@\n logger.info(f\"读取README文件: {readme_path}\")\n try:\n with open(readme_path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n logger.debug(f\"README内容长度: {len(content)} 字符\")\n- if (readme_path.parent / 'design.json').exists():\n- with open((readme_path.parent / 'design.json')) as f:\n- content += f'\\n\\ndesign.json(包含项目设计有关信息)内容如下:{f.read()}'\n+ if (readme_path.parent / \"design.json\").exists():\n+ with open((readme_path.parent / \"design.json\")) as f:\n+ content += (\n+ f\"\\n\\ndesign.json(包含项目设计有关信息)内容如下:{f.read()}\"\n+ )\n return content\n except Exception as e:\n logger.error(f\"读取README失败: {e}\")\n raise\n \n@@ -204,11 +208,13 @@\n else:\n logger.warning(FileNotFoundError(f\"依赖文件不存在: {dep}\"))\n \n with open(dep_path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n- context_content.append(f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\")\n+ context_content.append(\n+ f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\"\n+ )\n \n full_context = \"\\n\".join(context_content)\n \n system_prompt = (\n \"你是一个专业的编程助手。根据用户指令和提供的上下文文件,生成完整的代码。\"\n@@ -257,11 +263,10 @@\n except subprocess.TimeoutExpired:\n logger.error(f\"命令执行超时: {cmd}\")\n except Exception as e:\n logger.error(f\"命令执行失败: {e}\")\n \n-\n def run(self, readme_path: Path):\n \"\"\"\n 主执行流程\n \"\"\"\n logger.info(\"=\" * 50)\n@@ -300,11 +305,13 @@\n try:\n # 获取依赖文件\n deps = dependencies.get(file, [])\n \n # 构造生成指令\n- instruction = f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ instruction = (\n+ f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ )\n \n # 调用LLM生成代码\n code, desc, commands = self.generate_file(file, instruction, deps)\n \n logger.info(f\"生成完成: {file} - {desc}\")\n@@ -328,33 +335,47 @@\n progress.remove_task(file_task)\n progress.update(total_task, advance=1)\n \n logger.success(\"所有文件处理完成!\")\n \n+\n # ==================== CLI入口 ====================\n @app.command()\n def main(\n- readme: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"README.md文件路径\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"输出根目录,默认为readme所在目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥,也可通过环境变量DEEPSEEK_APIKEY设置\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ readme: Path = typer.Argument(\n+ ..., exists=True, file_okay=True, dir_okay=False, help=\"README.md文件路径\"\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"输出根目录,默认为readme所在目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None,\n+ \"--api-key\",\n+ envvar=\"DEEPSEEK_APIKEY\",\n+ help=\"API密钥,也可通过环境变量DEEPSEEK_APIKEY设置\",\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n- log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径(默认输出目录下generator.log)\"),\n+ log_file: Optional[str] = typer.Option(\n+ None, \"--log\", help=\"日志文件路径(默认输出目录下generator.log)\"\n+ ),\n ):\n \"\"\"\n 根据README自动生成项目代码\n \"\"\"\n if output_dir is None:\n output_dir = readme.parent\n \n generator = CodeGenerator(\n- api_key=api_key,\n- base_url=base_url,\n- model=model,\n- output_dir=output_dir,\n- log_file=log_file,\n- )\n+ api_key=api_key,\n+ base_url=base_url,\n+ model=model,\n+ output_dir=output_dir,\n+ log_file=log_file,\n+ )\n generator.run(readme)\n \n \n if __name__ == \"__main__\":\n app()\n", - "stderr": "would reformat llmcodegen.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat llmcodegen.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- llmcodegen.py\t2026-03-17 14:27:38.333874+00:00\n+++ llmcodegen.py\t2026-03-17 14:57:43.010113+00:00\n@@ -22,10 +22,11 @@\n DANGEROUS_COMMANDS = [\"rm\", \"sudo\", \"chmod\", \"dd\", \"mkfs\", \"> /dev/sda\", \"format\"]\n ALLOWED_COMMANDS = [] # 可设置白名单,为空则只检查黑名单\n \n app = typer.Typer(help=\"基于LLM的自动化代码生成工具\")\n console = Console()\n+\n \n # ==================== 工具函数 ====================\n def is_dangerous_command(cmd: str) -> Tuple[bool, str]:\n \"\"\"\n 判断命令是否危险\n@@ -35,10 +36,11 @@\n for danger in DANGEROUS_COMMANDS:\n if danger in cmd_lower:\n return True, f\"包含危险关键词 '{danger}'\"\n return False, \"\"\n \n+\n # ==================== 核心类 ====================\n class CodeGenerator:\n \"\"\"代码生成器,封装所有逻辑\"\"\"\n \n def __init__(\n@@ -136,13 +138,15 @@\n logger.info(f\"读取README文件: {readme_path}\")\n try:\n with open(readme_path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n logger.debug(f\"README内容长度: {len(content)} 字符\")\n- if (readme_path.parent / 'design.json').exists():\n- with open((readme_path.parent / 'design.json')) as f:\n- content += f'\\n\\ndesign.json(包含项目设计有关信息)内容如下:{f.read()}'\n+ if (readme_path.parent / \"design.json\").exists():\n+ with open((readme_path.parent / \"design.json\")) as f:\n+ content += (\n+ f\"\\n\\ndesign.json(包含项目设计有关信息)内容如下:{f.read()}\"\n+ )\n return content\n except Exception as e:\n logger.error(f\"读取README失败: {e}\")\n raise\n \n@@ -204,11 +208,13 @@\n else:\n logger.warning(FileNotFoundError(f\"依赖文件不存在: {dep}\"))\n \n with open(dep_path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n- context_content.append(f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\")\n+ context_content.append(\n+ f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\"\n+ )\n \n full_context = \"\\n\".join(context_content)\n \n system_prompt = (\n \"你是一个专业的编程助手。根据用户指令和提供的上下文文件,生成完整的代码。\"\n@@ -257,11 +263,10 @@\n except subprocess.TimeoutExpired:\n logger.error(f\"命令执行超时: {cmd}\")\n except Exception as e:\n logger.error(f\"命令执行失败: {e}\")\n \n-\n def run(self, readme_path: Path):\n \"\"\"\n 主执行流程\n \"\"\"\n logger.info(\"=\" * 50)\n@@ -300,11 +305,13 @@\n try:\n # 获取依赖文件\n deps = dependencies.get(file, [])\n \n # 构造生成指令\n- instruction = f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ instruction = (\n+ f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ )\n \n # 调用LLM生成代码\n code, desc, commands = self.generate_file(file, instruction, deps)\n \n logger.info(f\"生成完成: {file} - {desc}\")\n@@ -328,33 +335,47 @@\n progress.remove_task(file_task)\n progress.update(total_task, advance=1)\n \n logger.success(\"所有文件处理完成!\")\n \n+\n # ==================== CLI入口 ====================\n @app.command()\n def main(\n- readme: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"README.md文件路径\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"输出根目录,默认为readme所在目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥,也可通过环境变量DEEPSEEK_APIKEY设置\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ readme: Path = typer.Argument(\n+ ..., exists=True, file_okay=True, dir_okay=False, help=\"README.md文件路径\"\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"输出根目录,默认为readme所在目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None,\n+ \"--api-key\",\n+ envvar=\"DEEPSEEK_APIKEY\",\n+ help=\"API密钥,也可通过环境变量DEEPSEEK_APIKEY设置\",\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n- log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径(默认输出目录下generator.log)\"),\n+ log_file: Optional[str] = typer.Option(\n+ None, \"--log\", help=\"日志文件路径(默认输出目录下generator.log)\"\n+ ),\n ):\n \"\"\"\n 根据README自动生成项目代码\n \"\"\"\n if output_dir is None:\n output_dir = readme.parent\n \n generator = CodeGenerator(\n- api_key=api_key,\n- base_url=base_url,\n- model=model,\n- output_dir=output_dir,\n- log_file=log_file,\n- )\n+ api_key=api_key,\n+ base_url=base_url,\n+ model=model,\n+ output_dir=output_dir,\n+ log_file=log_file,\n+ )\n generator.run(readme)\n \n \n if __name__ == \"__main__\":\n app()" - ] - }, - { - "tool": "black", - "file": "src/llm_codegen/models.py", - "returncode": 1, - "stdout": "--- src/llm_codegen/models.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/models.py\t2026-03-17 14:57:43.085000+00:00\n@@ -3,34 +3,38 @@\n \n \n # 模型用于 design.json 结构\n class FunctionModel(BaseModel):\n \"\"\"函数模型,对应 design.json 中的 functions 字段。\"\"\"\n+\n name: str\n summary: str\n inputs: List[str]\n outputs: List[str]\n \n \n class ClassModel(BaseModel):\n \"\"\"类模型,对应 design.json 中的 classes 字段。\"\"\"\n+\n name: str\n summary: str\n methods: List[str]\n \n \n class FileModel(BaseModel):\n \"\"\"文件模型,对应 design.json 中的 files 字段。\"\"\"\n+\n path: str\n summary: str\n dependencies: List[str] = Field(default_factory=list)\n functions: List[FunctionModel] = Field(default_factory=list)\n classes: List[ClassModel] = Field(default_factory=list)\n \n \n class DesignModel(BaseModel):\n \"\"\"设计模型,对应 design.json 的根结构。\"\"\"\n+\n project_name: str\n version: str\n description: str\n files: List[FileModel]\n commands: List[str] = Field(default_factory=list)\n@@ -38,18 +42,20 @@\n \n \n # 模型用于工单\n class FeatureIssue(BaseModel):\n \"\"\"需求工单模型,基于 README 中的模板。\"\"\"\n+\n name: str\n description: str\n affected_files: Optional[List[str]] = Field(default_factory=list)\n acceptance_criteria: List[str]\n \n \n class BugIssue(BaseModel):\n \"\"\"Bug 工单模型,基于 README 中的模板。\"\"\"\n+\n name: str\n description: str\n steps_to_reproduce: List[str]\n expected_behavior: str\n actual_behavior: str\n@@ -57,10 +63,11 @@\n \n \n # 模型用于断点续写状态\n class StateModel(BaseModel):\n \"\"\"状态模型,用于保存生成过程中的断点状态。\"\"\"\n+\n current_file_index: int = 0\n generated_files: List[str] = Field(default_factory=list)\n dependencies_map: Dict[str, List[str]] = Field(default_factory=dict)\n total_files: int\n output_dir: str\n@@ -68,8 +75,9 @@\n \n \n # 可选:通用响应模型,用于 LLM 调用\n class LLMResponse(BaseModel):\n \"\"\"LLM 响应模型,用于解析 generate_file 方法的返回。\"\"\"\n+\n code: str\n description: str\n commands: List[str] = Field(default_factory=list)\n", - "stderr": "would reformat src/llm_codegen/models.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat src/llm_codegen/models.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- src/llm_codegen/models.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/models.py\t2026-03-17 14:57:43.085000+00:00\n@@ -3,34 +3,38 @@\n \n \n # 模型用于 design.json 结构\n class FunctionModel(BaseModel):\n \"\"\"函数模型,对应 design.json 中的 functions 字段。\"\"\"\n+\n name: str\n summary: str\n inputs: List[str]\n outputs: List[str]\n \n \n class ClassModel(BaseModel):\n \"\"\"类模型,对应 design.json 中的 classes 字段。\"\"\"\n+\n name: str\n summary: str\n methods: List[str]\n \n \n class FileModel(BaseModel):\n \"\"\"文件模型,对应 design.json 中的 files 字段。\"\"\"\n+\n path: str\n summary: str\n dependencies: List[str] = Field(default_factory=list)\n functions: List[FunctionModel] = Field(default_factory=list)\n classes: List[ClassModel] = Field(default_factory=list)\n \n \n class DesignModel(BaseModel):\n \"\"\"设计模型,对应 design.json 的根结构。\"\"\"\n+\n project_name: str\n version: str\n description: str\n files: List[FileModel]\n commands: List[str] = Field(default_factory=list)\n@@ -38,18 +42,20 @@\n \n \n # 模型用于工单\n class FeatureIssue(BaseModel):\n \"\"\"需求工单模型,基于 README 中的模板。\"\"\"\n+\n name: str\n description: str\n affected_files: Optional[List[str]] = Field(default_factory=list)\n acceptance_criteria: List[str]\n \n \n class BugIssue(BaseModel):\n \"\"\"Bug 工单模型,基于 README 中的模板。\"\"\"\n+\n name: str\n description: str\n steps_to_reproduce: List[str]\n expected_behavior: str\n actual_behavior: str\n@@ -57,10 +63,11 @@\n \n \n # 模型用于断点续写状态\n class StateModel(BaseModel):\n \"\"\"状态模型,用于保存生成过程中的断点状态。\"\"\"\n+\n current_file_index: int = 0\n generated_files: List[str] = Field(default_factory=list)\n dependencies_map: Dict[str, List[str]] = Field(default_factory=dict)\n total_files: int\n output_dir: str\n@@ -68,8 +75,9 @@\n \n \n # 可选:通用响应模型,用于 LLM 调用\n class LLMResponse(BaseModel):\n \"\"\"LLM 响应模型,用于解析 generate_file 方法的返回。\"\"\"\n+\n code: str\n description: str\n commands: List[str] = Field(default_factory=list)" - ] - }, - { - "tool": "black", - "file": "tests/test_checker.py", - "returncode": 1, - "stdout": "--- tests/test_checker.py\t2026-03-17 14:57:24.156832+00:00\n+++ tests/test_checker.py\t2026-03-17 14:57:43.083978+00:00\n@@ -9,10 +9,11 @@\n \n \n # ---------- Fake 对象 ----------\n class FakeCodeGenerator:\n \"\"\"假的 CodeGenerator,用于替代真实的 LLM 调用\"\"\"\n+\n def __init__(self, return_value=None):\n self._call_llm_called = False\n self._call_llm_args = None\n self.return_value = return_value or {\"patches\": [], \"description\": \"模拟修复\"}\n \n@@ -56,15 +57,13 @@\n file_path = Path(\"test_file.py\")\n \n # 模拟 subprocess.run 返回成功\n def fake_run(cmd, *args, **kwargs):\n return subprocess.CompletedProcess(\n- args=cmd,\n- returncode=0,\n- stdout=\"\",\n- stderr=\"\"\n+ args=cmd, returncode=0, stdout=\"\", stderr=\"\"\n )\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run)\n \n result = checker.run_check(\"pylint\", file_path)\n \n assert result[\"tool\"] == \"pylint\"\n@@ -77,10 +76,11 @@\n file_path = Path(\"test_file.py\")\n \n # 让 subprocess.run 抛出超时异常\n def fake_run_timeout(*args, **kwargs):\n raise subprocess.TimeoutExpired(cmd=\"pylint\", timeout=60)\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run_timeout)\n \n result = checker.run_check(\"pylint\", file_path)\n \n assert result[\"returncode\"] == -1\n@@ -91,19 +91,42 @@\n test_file = tmp_path / \"test.py\"\n test_file.write_text(\"print('hello')\\n\")\n \n # 替换 run_check 方法,避免真正执行\n fake_results = [\n- {\"tool\": \"pylint\", \"file\": str(test_file), \"returncode\": 0, \"stdout\": \"\", \"stderr\": \"\", \"errors\": []},\n- {\"tool\": \"mypy\", \"file\": str(test_file), \"returncode\": 0, \"stdout\": \"\", \"stderr\": \"\", \"errors\": []},\n- {\"tool\": \"black\", \"file\": str(test_file), \"returncode\": 0, \"stdout\": \"\", \"stderr\": \"\", \"errors\": []}\n+ {\n+ \"tool\": \"pylint\",\n+ \"file\": str(test_file),\n+ \"returncode\": 0,\n+ \"stdout\": \"\",\n+ \"stderr\": \"\",\n+ \"errors\": [],\n+ },\n+ {\n+ \"tool\": \"mypy\",\n+ \"file\": str(test_file),\n+ \"returncode\": 0,\n+ \"stdout\": \"\",\n+ \"stderr\": \"\",\n+ \"errors\": [],\n+ },\n+ {\n+ \"tool\": \"black\",\n+ \"file\": str(test_file),\n+ \"returncode\": 0,\n+ \"stdout\": \"\",\n+ \"stderr\": \"\",\n+ \"errors\": [],\n+ },\n ]\n call_count = 0\n+\n def fake_run_check(tool, file):\n nonlocal call_count\n call_count += 1\n return fake_results[call_count - 1]\n+\n monkeypatch.setattr(checker, \"run_check\", fake_run_check)\n \n results = checker.run_parallel_checks([test_file])\n \n assert len(results) == 1\n@@ -115,11 +138,11 @@\n results = [{\"tool\": \"pylint\", \"file\": \"file1.py\", \"returncode\": 0}]\n checker.save_results(results)\n \n results_file = checker.output_dir / \"check_results.json\"\n assert results_file.exists()\n- with open(results_file, 'r') as f:\n+ with open(results_file, \"r\") as f:\n loaded = json.load(f)\n assert loaded == results\n \n def test_collect_errors(self, checker, tmp_path):\n \"\"\"测试收集错误\"\"\"\n@@ -171,11 +194,11 @@\n checker.code_generator.return_value = fake_return\n \n success = checker.auto_fix(errors, context_files=[\"test.py\"])\n \n assert success is True\n- with open(test_file, 'r') as f:\n+ with open(test_file, \"r\") as f:\n assert f.read() == \"print('hi')\\n\"\n assert checker.code_generator._call_llm_called is True\n \n def test_auto_fix_no_errors(self, checker):\n \"\"\"测试自动修复无错误时\"\"\"\n", - "stderr": "would reformat tests/test_checker.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat tests/test_checker.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- tests/test_checker.py\t2026-03-17 14:57:24.156832+00:00\n+++ tests/test_checker.py\t2026-03-17 14:57:43.083978+00:00\n@@ -9,10 +9,11 @@\n \n \n # ---------- Fake 对象 ----------\n class FakeCodeGenerator:\n \"\"\"假的 CodeGenerator,用于替代真实的 LLM 调用\"\"\"\n+\n def __init__(self, return_value=None):\n self._call_llm_called = False\n self._call_llm_args = None\n self.return_value = return_value or {\"patches\": [], \"description\": \"模拟修复\"}\n \n@@ -56,15 +57,13 @@\n file_path = Path(\"test_file.py\")\n \n # 模拟 subprocess.run 返回成功\n def fake_run(cmd, *args, **kwargs):\n return subprocess.CompletedProcess(\n- args=cmd,\n- returncode=0,\n- stdout=\"\",\n- stderr=\"\"\n+ args=cmd, returncode=0, stdout=\"\", stderr=\"\"\n )\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run)\n \n result = checker.run_check(\"pylint\", file_path)\n \n assert result[\"tool\"] == \"pylint\"\n@@ -77,10 +76,11 @@\n file_path = Path(\"test_file.py\")\n \n # 让 subprocess.run 抛出超时异常\n def fake_run_timeout(*args, **kwargs):\n raise subprocess.TimeoutExpired(cmd=\"pylint\", timeout=60)\n+\n monkeypatch.setattr(subprocess, \"run\", fake_run_timeout)\n \n result = checker.run_check(\"pylint\", file_path)\n \n assert result[\"returncode\"] == -1\n@@ -91,19 +91,42 @@\n test_file = tmp_path / \"test.py\"\n test_file.write_text(\"print('hello')\\n\")\n \n # 替换 run_check 方法,避免真正执行\n fake_results = [\n- {\"tool\": \"pylint\", \"file\": str(test_file), \"returncode\": 0, \"stdout\": \"\", \"stderr\": \"\", \"errors\": []},\n- {\"tool\": \"mypy\", \"file\": str(test_file), \"returncode\": 0, \"stdout\": \"\", \"stderr\": \"\", \"errors\": []},\n- {\"tool\": \"black\", \"file\": str(test_file), \"returncode\": 0, \"stdout\": \"\", \"stderr\": \"\", \"errors\": []}\n+ {\n+ \"tool\": \"pylint\",\n+ \"file\": str(test_file),\n+ \"returncode\": 0,\n+ \"stdout\": \"\",\n+ \"stderr\": \"\",\n+ \"errors\": [],\n+ },\n+ {\n+ \"tool\": \"mypy\",\n+ \"file\": str(test_file),\n+ \"returncode\": 0,\n+ \"stdout\": \"\",\n+ \"stderr\": \"\",\n+ \"errors\": [],\n+ },\n+ {\n+ \"tool\": \"black\",\n+ \"file\": str(test_file),\n+ \"returncode\": 0,\n+ \"stdout\": \"\",\n+ \"stderr\": \"\",\n+ \"errors\": [],\n+ },\n ]\n call_count = 0\n+\n def fake_run_check(tool, file):\n nonlocal call_count\n call_count += 1\n return fake_results[call_count - 1]\n+\n monkeypatch.setattr(checker, \"run_check\", fake_run_check)\n \n results = checker.run_parallel_checks([test_file])\n \n assert len(results) == 1\n@@ -115,11 +138,11 @@\n results = [{\"tool\": \"pylint\", \"file\": \"file1.py\", \"returncode\": 0}]\n checker.save_results(results)\n \n results_file = checker.output_dir / \"check_results.json\"\n assert results_file.exists()\n- with open(results_file, 'r') as f:\n+ with open(results_file, \"r\") as f:\n loaded = json.load(f)\n assert loaded == results\n \n def test_collect_errors(self, checker, tmp_path):\n \"\"\"测试收集错误\"\"\"\n@@ -171,11 +194,11 @@\n checker.code_generator.return_value = fake_return\n \n success = checker.auto_fix(errors, context_files=[\"test.py\"])\n \n assert success is True\n- with open(test_file, 'r') as f:\n+ with open(test_file, \"r\") as f:\n assert f.read() == \"print('hi')\\n\"\n assert checker.code_generator._call_llm_called is True\n \n def test_auto_fix_no_errors(self, checker):\n \"\"\"测试自动修复无错误时\"\"\"" - ] - }, - { - "tool": "black", - "file": "src/llm_codegen/__init__.py", - "returncode": 1, - "stdout": "--- src/llm_codegen/__init__.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/__init__.py\t2026-03-17 14:57:43.116906+00:00\n@@ -8,8 +8,9 @@\n __version__ = \"1.0.0\"\n __description__ = \"一个基于大语言模型的智能代码生成与维护工具\"\n \n # 导出核心模块以便从包级别导入\n from .core import CodeGenerator\n+\n # from .cli import main\n \n __all__ = [\"CodeGenerator\", \"__version__\", \"__description__\"]\n", - "stderr": "would reformat src/llm_codegen/__init__.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat src/llm_codegen/__init__.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- src/llm_codegen/__init__.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/__init__.py\t2026-03-17 14:57:43.116906+00:00\n@@ -8,8 +8,9 @@\n __version__ = \"1.0.0\"\n __description__ = \"一个基于大语言模型的智能代码生成与维护工具\"\n \n # 导出核心模块以便从包级别导入\n from .core import CodeGenerator\n+\n # from .cli import main\n \n __all__ = [\"CodeGenerator\", \"__version__\", \"__description__\"]" - ] - }, - { - "tool": "black", - "file": "src/llm_codegen/utils.py", - "returncode": 1, - "stdout": "--- src/llm_codegen/utils.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/utils.py\t2026-03-17 14:57:43.297071+00:00\n@@ -8,14 +8,14 @@\n \n \n def is_dangerous_command(cmd: str) -> Tuple[bool, str]:\n \"\"\"\n 判断命令是否危险\n- \n+\n Args:\n cmd: 命令字符串\n- \n+\n Returns:\n Tuple[bool, str]: (是否危险, 原因)\n \"\"\"\n cmd_lower = cmd.lower()\n for danger in DANGEROUS_COMMANDS:\n@@ -25,59 +25,59 @@\n \n \n def read_file(file_path: str) -> str:\n \"\"\"\n 读取文件内容\n- \n+\n Args:\n file_path: 文件路径\n- \n+\n Returns:\n str: 文件内容\n \"\"\"\n try:\n- with open(file_path, 'r', encoding='utf-8') as f:\n+ with open(file_path, \"r\", encoding=\"utf-8\") as f:\n return f.read()\n except Exception as e:\n raise IOError(f\"读取文件失败: {file_path}, 错误: {e}\")\n \n \n def write_file(file_path: str, content: str) -> None:\n \"\"\"\n 写入文件内容\n- \n+\n Args:\n file_path: 文件路径\n content: 要写入的内容\n \"\"\"\n try:\n path = Path(file_path)\n path.parent.mkdir(parents=True, exist_ok=True)\n- with open(file_path, 'w', encoding='utf-8') as f:\n+ with open(file_path, \"w\", encoding=\"utf-8\") as f:\n f.write(content)\n except Exception as e:\n raise IOError(f\"写入文件失败: {file_path}, 错误: {e}\")\n \n \n def ensure_dir(directory: str) -> None:\n \"\"\"\n 确保目录存在,如果不存在则创建\n- \n+\n Args:\n directory: 目录路径\n \"\"\"\n os.makedirs(directory, exist_ok=True)\n \n \n def safe_join(base_path: str, *paths: str) -> str:\n \"\"\"\n 安全地拼接路径,防止目录遍历攻击\n- \n+\n Args:\n base_path: 基础路径\n *paths: 要拼接的部分\n- \n+\n Returns:\n str: 拼接后的绝对路径\n \"\"\"\n full_path = os.path.abspath(os.path.join(base_path, *paths))\n base_abs = os.path.abspath(base_path)\n", - "stderr": "would reformat src/llm_codegen/utils.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat src/llm_codegen/utils.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- src/llm_codegen/utils.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/utils.py\t2026-03-17 14:57:43.297071+00:00\n@@ -8,14 +8,14 @@\n \n \n def is_dangerous_command(cmd: str) -> Tuple[bool, str]:\n \"\"\"\n 判断命令是否危险\n- \n+\n Args:\n cmd: 命令字符串\n- \n+\n Returns:\n Tuple[bool, str]: (是否危险, 原因)\n \"\"\"\n cmd_lower = cmd.lower()\n for danger in DANGEROUS_COMMANDS:\n@@ -25,59 +25,59 @@\n \n \n def read_file(file_path: str) -> str:\n \"\"\"\n 读取文件内容\n- \n+\n Args:\n file_path: 文件路径\n- \n+\n Returns:\n str: 文件内容\n \"\"\"\n try:\n- with open(file_path, 'r', encoding='utf-8') as f:\n+ with open(file_path, \"r\", encoding=\"utf-8\") as f:\n return f.read()\n except Exception as e:\n raise IOError(f\"读取文件失败: {file_path}, 错误: {e}\")\n \n \n def write_file(file_path: str, content: str) -> None:\n \"\"\"\n 写入文件内容\n- \n+\n Args:\n file_path: 文件路径\n content: 要写入的内容\n \"\"\"\n try:\n path = Path(file_path)\n path.parent.mkdir(parents=True, exist_ok=True)\n- with open(file_path, 'w', encoding='utf-8') as f:\n+ with open(file_path, \"w\", encoding=\"utf-8\") as f:\n f.write(content)\n except Exception as e:\n raise IOError(f\"写入文件失败: {file_path}, 错误: {e}\")\n \n \n def ensure_dir(directory: str) -> None:\n \"\"\"\n 确保目录存在,如果不存在则创建\n- \n+\n Args:\n directory: 目录路径\n \"\"\"\n os.makedirs(directory, exist_ok=True)\n \n \n def safe_join(base_path: str, *paths: str) -> str:\n \"\"\"\n 安全地拼接路径,防止目录遍历攻击\n- \n+\n Args:\n base_path: 基础路径\n *paths: 要拼接的部分\n- \n+\n Returns:\n str: 拼接后的绝对路径\n \"\"\"\n full_path = os.path.abspath(os.path.join(base_path, *paths))\n base_abs = os.path.abspath(base_path)" - ] - }, - { - "tool": "black", - "file": "src/llm_codegen/cli.py", - "returncode": 1, - "stdout": "--- src/llm_codegen/cli.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/cli.py\t2026-03-17 14:57:43.351695+00:00\n@@ -19,23 +19,31 @@\n console = Console()\n \n \n @app.command()\n def init(\n- readme: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"README.md 文件路径\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"输出根目录,默认为当前目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ readme: Path = typer.Argument(\n+ ..., exists=True, file_okay=True, dir_okay=False, help=\"README.md 文件路径\"\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"输出根目录,默认为当前目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径\"),\n ):\n \"\"\"\n 初始化项目:根据 README.md 自动生成完整的代码。\n \"\"\"\n if output_dir is None:\n output_dir = Path.cwd()\n- \n+\n try:\n generator = CodeGenerator(\n api_key=api_key,\n base_url=base_url,\n model=model,\n@@ -48,48 +56,64 @@\n raise typer.Exit(code=1)\n \n \n @app.command()\n def enhance(\n- issue_file: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"需求工单文件路径(如 feature.issue)\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ issue_file: Path = typer.Argument(\n+ ...,\n+ exists=True,\n+ file_okay=True,\n+ dir_okay=False,\n+ help=\"需求工单文件路径(如 feature.issue)\",\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径\"),\n ):\n \"\"\"\n 增强项目:根据需求工单添加新功能。\n \"\"\"\n if output_dir is None:\n output_dir = Path.cwd()\n- \n+\n # 读取工单文件\n try:\n with open(issue_file, \"r\", encoding=\"utf-8\") as f:\n issue_content = f.read()\n except Exception as e:\n logger.error(f\"读取工单文件失败: {e}\")\n raise typer.Exit(code=1)\n- \n+\n # 检查 design.json 是否存在\n design_path = output_dir / \"design.json\"\n if not design_path.exists():\n- logger.error(f\"design.json 不存在于 {output_dir},请先运行 init 命令初始化项目。\")\n+ logger.error(\n+ f\"design.json 不存在于 {output_dir},请先运行 init 命令初始化项目。\"\n+ )\n raise typer.Exit(code=1)\n- \n+\n try:\n generator = CodeGenerator(\n api_key=api_key,\n base_url=base_url,\n model=model,\n output_dir=str(output_dir),\n log_file=log_file,\n )\n # 简化增强逻辑:基于工单内容调用 LLM 生成代码变更\n logger.info(f\"处理增强工单: {issue_file}\")\n- console.print(f\"[yellow]注意:增强功能为简化实现,基于工单内容生成变更。工单内容预览: {issue_content[:100]}...[/yellow]\")\n+ console.print(\n+ f\"[yellow]注意:增强功能为简化实现,基于工单内容生成变更。工单内容预览: {issue_content[:100]}...[/yellow]\"\n+ )\n # 实际应用中,这里应解析工单并调用 generator 或类似方法生成代码\n # 示例:生成一个占位文件或调用检查器\n checker = Checker(output_dir=output_dir, code_generator=generator)\n success = checker.run_full_check_and_fix()\n if not success:\n@@ -101,48 +125,62 @@\n raise typer.Exit(code=1)\n \n \n @app.command()\n def fix(\n- issue_file: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"Bug工单文件路径(如 bug.issue)\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ issue_file: Path = typer.Argument(\n+ ...,\n+ exists=True,\n+ file_okay=True,\n+ dir_okay=False,\n+ help=\"Bug工单文件路径(如 bug.issue)\",\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径\"),\n ):\n \"\"\"\n 修复项目:根据Bug工单自动修复 Bug。\n \"\"\"\n if output_dir is None:\n output_dir = Path.cwd()\n- \n+\n # 读取工单文件\n try:\n with open(issue_file, \"r\", encoding=\"utf-8\") as f:\n issue_content = f.read()\n except Exception as e:\n logger.error(f\"读取工单文件失败: {e}\")\n raise typer.Exit(code=1)\n- \n+\n # 检查 design.json 是否存在\n design_path = output_dir / \"design.json\"\n if not design_path.exists():\n logger.error(f\"design.json 不存在于 {output_dir},请确保项目已初始化。\")\n raise typer.Exit(code=1)\n- \n+\n try:\n generator = CodeGenerator(\n api_key=api_key,\n base_url=base_url,\n model=model,\n output_dir=str(output_dir),\n log_file=log_file,\n )\n # 简化修复逻辑:基于工单内容调用检查器进行修复\n logger.info(f\"处理Bug工单: {issue_file}\")\n- console.print(f\"[yellow]注意:修复功能为简化实现,基于工单内容调用检查器。工单内容预览: {issue_content[:100]}...[/yellow]\")\n+ console.print(\n+ f\"[yellow]注意:修复功能为简化实现,基于工单内容调用检查器。工单内容预览: {issue_content[:100]}...[/yellow]\"\n+ )\n checker = Checker(output_dir=output_dir, code_generator=generator)\n success = checker.run_full_check_and_fix()\n if not success:\n logger.error(\"修复过程中检查失败\")\n raise typer.Exit(code=1)\n", - "stderr": "would reformat src/llm_codegen/cli.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat src/llm_codegen/cli.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- src/llm_codegen/cli.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/cli.py\t2026-03-17 14:57:43.351695+00:00\n@@ -19,23 +19,31 @@\n console = Console()\n \n \n @app.command()\n def init(\n- readme: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"README.md 文件路径\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"输出根目录,默认为当前目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ readme: Path = typer.Argument(\n+ ..., exists=True, file_okay=True, dir_okay=False, help=\"README.md 文件路径\"\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"输出根目录,默认为当前目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径\"),\n ):\n \"\"\"\n 初始化项目:根据 README.md 自动生成完整的代码。\n \"\"\"\n if output_dir is None:\n output_dir = Path.cwd()\n- \n+\n try:\n generator = CodeGenerator(\n api_key=api_key,\n base_url=base_url,\n model=model,\n@@ -48,48 +56,64 @@\n raise typer.Exit(code=1)\n \n \n @app.command()\n def enhance(\n- issue_file: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"需求工单文件路径(如 feature.issue)\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ issue_file: Path = typer.Argument(\n+ ...,\n+ exists=True,\n+ file_okay=True,\n+ dir_okay=False,\n+ help=\"需求工单文件路径(如 feature.issue)\",\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径\"),\n ):\n \"\"\"\n 增强项目:根据需求工单添加新功能。\n \"\"\"\n if output_dir is None:\n output_dir = Path.cwd()\n- \n+\n # 读取工单文件\n try:\n with open(issue_file, \"r\", encoding=\"utf-8\") as f:\n issue_content = f.read()\n except Exception as e:\n logger.error(f\"读取工单文件失败: {e}\")\n raise typer.Exit(code=1)\n- \n+\n # 检查 design.json 是否存在\n design_path = output_dir / \"design.json\"\n if not design_path.exists():\n- logger.error(f\"design.json 不存在于 {output_dir},请先运行 init 命令初始化项目。\")\n+ logger.error(\n+ f\"design.json 不存在于 {output_dir},请先运行 init 命令初始化项目。\"\n+ )\n raise typer.Exit(code=1)\n- \n+\n try:\n generator = CodeGenerator(\n api_key=api_key,\n base_url=base_url,\n model=model,\n output_dir=str(output_dir),\n log_file=log_file,\n )\n # 简化增强逻辑:基于工单内容调用 LLM 生成代码变更\n logger.info(f\"处理增强工单: {issue_file}\")\n- console.print(f\"[yellow]注意:增强功能为简化实现,基于工单内容生成变更。工单内容预览: {issue_content[:100]}...[/yellow]\")\n+ console.print(\n+ f\"[yellow]注意:增强功能为简化实现,基于工单内容生成变更。工单内容预览: {issue_content[:100]}...[/yellow]\"\n+ )\n # 实际应用中,这里应解析工单并调用 generator 或类似方法生成代码\n # 示例:生成一个占位文件或调用检查器\n checker = Checker(output_dir=output_dir, code_generator=generator)\n success = checker.run_full_check_and_fix()\n if not success:\n@@ -101,48 +125,62 @@\n raise typer.Exit(code=1)\n \n \n @app.command()\n def fix(\n- issue_file: Path = typer.Argument(..., exists=True, file_okay=True, dir_okay=False, help=\"Bug工单文件路径(如 bug.issue)\"),\n- output_dir: Optional[Path] = typer.Option(None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"),\n- api_key: Optional[str] = typer.Option(None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"),\n- base_url: str = typer.Option(\"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"),\n+ issue_file: Path = typer.Argument(\n+ ...,\n+ exists=True,\n+ file_okay=True,\n+ dir_okay=False,\n+ help=\"Bug工单文件路径(如 bug.issue)\",\n+ ),\n+ output_dir: Optional[Path] = typer.Option(\n+ None, \"--output\", \"-o\", help=\"项目根目录,默认为当前目录\"\n+ ),\n+ api_key: Optional[str] = typer.Option(\n+ None, \"--api-key\", envvar=\"DEEPSEEK_APIKEY\", help=\"API密钥\"\n+ ),\n+ base_url: str = typer.Option(\n+ \"https://api.deepseek.com\", \"--base-url\", help=\"API基础URL\"\n+ ),\n model: str = typer.Option(\"deepseek-reasoner\", \"--model\", \"-m\", help=\"使用的模型\"),\n log_file: Optional[str] = typer.Option(None, \"--log\", help=\"日志文件路径\"),\n ):\n \"\"\"\n 修复项目:根据Bug工单自动修复 Bug。\n \"\"\"\n if output_dir is None:\n output_dir = Path.cwd()\n- \n+\n # 读取工单文件\n try:\n with open(issue_file, \"r\", encoding=\"utf-8\") as f:\n issue_content = f.read()\n except Exception as e:\n logger.error(f\"读取工单文件失败: {e}\")\n raise typer.Exit(code=1)\n- \n+\n # 检查 design.json 是否存在\n design_path = output_dir / \"design.json\"\n if not design_path.exists():\n logger.error(f\"design.json 不存在于 {output_dir},请确保项目已初始化。\")\n raise typer.Exit(code=1)\n- \n+\n try:\n generator = CodeGenerator(\n api_key=api_key,\n base_url=base_url,\n model=model,\n output_dir=str(output_dir),\n log_file=log_file,\n )\n # 简化修复逻辑:基于工单内容调用检查器进行修复\n logger.info(f\"处理Bug工单: {issue_file}\")\n- console.print(f\"[yellow]注意:修复功能为简化实现,基于工单内容调用检查器。工单内容预览: {issue_content[:100]}...[/yellow]\")\n+ console.print(\n+ f\"[yellow]注意:修复功能为简化实现,基于工单内容调用检查器。工单内容预览: {issue_content[:100]}...[/yellow]\"\n+ )\n checker = Checker(output_dir=output_dir, code_generator=generator)\n success = checker.run_full_check_and_fix()\n if not success:\n logger.error(\"修复过程中检查失败\")\n raise typer.Exit(code=1)" - ] - }, - { - "tool": "black", - "file": "src/llm_codegen/checker.py", - "returncode": 1, - "stdout": "--- src/llm_codegen/checker.py\t2026-03-17 14:52:45.835518+00:00\n+++ src/llm_codegen/checker.py\t2026-03-17 14:57:43.402370+00:00\n@@ -12,14 +12,16 @@\n \n # 尝试导入 pathspec(用于精确解析 .gitignore)\n try:\n from pathspec import PathSpec\n from pathspec.patterns import GitWildMatchPattern\n+\n HAS_PATHSPEC = True\n except ImportError:\n HAS_PATHSPEC = False\n import fnmatch\n+\n warnings.warn(\n \"pathspec 未安装,将使用简单的通配符匹配处理 .gitignore(可能不完全准确)。\"\n \"建议安装:pip install pathspec\"\n )\n \n@@ -69,11 +71,13 @@\n model=model,\n output_dir=str(self.output_dir),\n )\n \n self.results_file = self.output_dir / \"check_results.json\"\n- logger.info(f\"Checker 初始化完成,输出目录: {self.output_dir},检查工具: {self.check_tools}\")\n+ logger.info(\n+ f\"Checker 初始化完成,输出目录: {self.output_dir},检查工具: {self.check_tools}\"\n+ )\n \n def _load_gitignore_patterns(self) -> Optional[Any]:\n \"\"\"\n 加载 .gitignore 文件中的模式,返回一个可用于匹配的函数或对象。\n 若文件不存在或解析失败,返回 None。\n@@ -128,11 +132,13 @@\n # 简单匹配:对于每个模式,如果模式以 / 结尾,则匹配目录;否则匹配文件\n for pattern in gitignore_matcher:\n # 处理目录模式(以 / 结尾)\n if pattern.endswith(\"/\"):\n # 检查路径是否以该目录开头\n- if rel_path.startswith(pattern.rstrip(\"/\") + \"/\") or rel_path == pattern.rstrip(\"/\"):\n+ if rel_path.startswith(\n+ pattern.rstrip(\"/\") + \"/\"\n+ ) or rel_path == pattern.rstrip(\"/\"):\n return True\n else:\n # 文件/通配符模式,使用 fnmatch\n if fnmatch.fnmatch(rel_path, pattern):\n return True\n@@ -148,11 +154,13 @@\n try:\n # 计算相对于输出目录的路径\n rel_path = file_path.relative_to(self.output_dir).as_posix()\n except ValueError:\n # 如果文件不在输出目录下(例如绝对路径),则保留(不过滤)\n- logger.warning(f\"文件 {file_path} 不在输出目录 {self.output_dir} 下,将保留\")\n+ logger.warning(\n+ f\"文件 {file_path} 不在输出目录 {self.output_dir} 下,将保留\"\n+ )\n filtered.append(file_path)\n continue\n \n # 硬编码忽略 .git 目录\n if rel_path.startswith(\".git/\") or rel_path == \".git\":\n@@ -244,11 +252,13 @@\n \"stdout\": \"\",\n \"stderr\": str(e),\n \"errors\": [str(e)],\n }\n \n- def run_parallel_checks(self, files: Optional[List[Path]] = None) -> List[Dict[str, Any]]:\n+ def run_parallel_checks(\n+ self, files: Optional[List[Path]] = None\n+ ) -> List[Dict[str, Any]]:\n \"\"\"\n 并行运行检查工具在指定文件上(仅使用配置的第一个工具)\n \n Args:\n files: 要检查的文件路径列表,如果为 None 则自动查找输出目录下所有 .py 文件(排除 .gitignore 中的)\n@@ -266,11 +276,13 @@\n tool = self.check_tools[0]\n logger.info(f\"开始并行检查,文件数: {len(files)},工具: {tool}\")\n \n all_results = []\n with ThreadPoolExecutor(max_workers=min(4, len(files))) as executor:\n- futures = [executor.submit(self.run_check, tool, file_path) for file_path in files]\n+ futures = [\n+ executor.submit(self.run_check, tool, file_path) for file_path in files\n+ ]\n \n for future in as_completed(futures):\n try:\n result = future.result()\n all_results.append(result)\n@@ -289,11 +301,13 @@\n json.dump(results, f, indent=2, ensure_ascii=False)\n logger.debug(f\"检查结果已保存至: {self.results_file}\")\n except Exception as e:\n logger.error(f\"保存检查结果失败: {e}\")\n \n- def collect_errors(self, results: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, Any]]:\n+ def collect_errors(\n+ self, results: Optional[List[Dict[str, Any]]] = None\n+ ) -> List[Dict[str, Any]]:\n \"\"\"\n 从检查结果中收集所有错误\n \n Args:\n results: 检查结果列表,如果为 None 则从文件加载\n@@ -316,19 +330,23 @@\n errors = []\n for result in results:\n if result.get(\"errors\") and result[\"errors\"]:\n for error_msg in result[\"errors\"]:\n if error_msg: # 跳过空错误\n- errors.append({\n- \"file\": result[\"file\"],\n- \"tool\": result[\"tool\"],\n- \"error\": error_msg,\n- })\n+ errors.append(\n+ {\n+ \"file\": result[\"file\"],\n+ \"tool\": result[\"tool\"],\n+ \"error\": error_msg,\n+ }\n+ )\n logger.info(f\"收集到 {len(errors)} 个错误\")\n return errors\n \n- def auto_fix(self, errors: List[Dict[str, Any]], context_files: Optional[List[str]] = None) -> bool:\n+ def auto_fix(\n+ self, errors: List[Dict[str, Any]], context_files: Optional[List[str]] = None\n+ ) -> bool:\n \"\"\"\n 自动调用 LLM 生成修复补丁并应用\n \n Args:\n errors: 错误列表,来自 collect_errors\n@@ -365,11 +383,13 @@\n path = Path(file_path)\n if not path.exists():\n path = self.output_dir / file_path\n if path.exists():\n with open(path, \"r\", encoding=\"utf-8\") as f:\n- context_content.append(f\"### 文件: {path.name} (路径: {file_path}) ###\\n{f.read()}\\n\")\n+ context_content.append(\n+ f\"### 文件: {path.name} (路径: {file_path}) ###\\n{f.read()}\\n\"\n+ )\n \n # 添加错误信息\n errors_str = json.dumps(errors, indent=2, ensure_ascii=False)\n context_content.append(f\"### 检查错误列表 ###\\n{errors_str}\\n\")\n \n@@ -384,11 +404,13 @@\n \"注意:只修复提到的错误,保持代码风格一致。\"\n )\n user_prompt = f\"请修复以下检查错误:\\n\\n{full_context}\"\n \n try:\n- result = self.code_generator._call_llm(system_prompt, user_prompt, temperature=0.1)\n+ result = self.code_generator._call_llm(\n+ system_prompt, user_prompt, temperature=0.1\n+ )\n patches = result.get(\"patches\", [])\n description = result.get(\"description\", \"无描述\")\n logger.info(f\"LLM 生成修复补丁: {description}, 补丁数: {len(patches)}\")\n \n # 应用补丁\n", - "stderr": "would reformat src/llm_codegen/checker.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat src/llm_codegen/checker.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- src/llm_codegen/checker.py\t2026-03-17 14:52:45.835518+00:00\n+++ src/llm_codegen/checker.py\t2026-03-17 14:57:43.402370+00:00\n@@ -12,14 +12,16 @@\n \n # 尝试导入 pathspec(用于精确解析 .gitignore)\n try:\n from pathspec import PathSpec\n from pathspec.patterns import GitWildMatchPattern\n+\n HAS_PATHSPEC = True\n except ImportError:\n HAS_PATHSPEC = False\n import fnmatch\n+\n warnings.warn(\n \"pathspec 未安装,将使用简单的通配符匹配处理 .gitignore(可能不完全准确)。\"\n \"建议安装:pip install pathspec\"\n )\n \n@@ -69,11 +71,13 @@\n model=model,\n output_dir=str(self.output_dir),\n )\n \n self.results_file = self.output_dir / \"check_results.json\"\n- logger.info(f\"Checker 初始化完成,输出目录: {self.output_dir},检查工具: {self.check_tools}\")\n+ logger.info(\n+ f\"Checker 初始化完成,输出目录: {self.output_dir},检查工具: {self.check_tools}\"\n+ )\n \n def _load_gitignore_patterns(self) -> Optional[Any]:\n \"\"\"\n 加载 .gitignore 文件中的模式,返回一个可用于匹配的函数或对象。\n 若文件不存在或解析失败,返回 None。\n@@ -128,11 +132,13 @@\n # 简单匹配:对于每个模式,如果模式以 / 结尾,则匹配目录;否则匹配文件\n for pattern in gitignore_matcher:\n # 处理目录模式(以 / 结尾)\n if pattern.endswith(\"/\"):\n # 检查路径是否以该目录开头\n- if rel_path.startswith(pattern.rstrip(\"/\") + \"/\") or rel_path == pattern.rstrip(\"/\"):\n+ if rel_path.startswith(\n+ pattern.rstrip(\"/\") + \"/\"\n+ ) or rel_path == pattern.rstrip(\"/\"):\n return True\n else:\n # 文件/通配符模式,使用 fnmatch\n if fnmatch.fnmatch(rel_path, pattern):\n return True\n@@ -148,11 +154,13 @@\n try:\n # 计算相对于输出目录的路径\n rel_path = file_path.relative_to(self.output_dir).as_posix()\n except ValueError:\n # 如果文件不在输出目录下(例如绝对路径),则保留(不过滤)\n- logger.warning(f\"文件 {file_path} 不在输出目录 {self.output_dir} 下,将保留\")\n+ logger.warning(\n+ f\"文件 {file_path} 不在输出目录 {self.output_dir} 下,将保留\"\n+ )\n filtered.append(file_path)\n continue\n \n # 硬编码忽略 .git 目录\n if rel_path.startswith(\".git/\") or rel_path == \".git\":\n@@ -244,11 +252,13 @@\n \"stdout\": \"\",\n \"stderr\": str(e),\n \"errors\": [str(e)],\n }\n \n- def run_parallel_checks(self, files: Optional[List[Path]] = None) -> List[Dict[str, Any]]:\n+ def run_parallel_checks(\n+ self, files: Optional[List[Path]] = None\n+ ) -> List[Dict[str, Any]]:\n \"\"\"\n 并行运行检查工具在指定文件上(仅使用配置的第一个工具)\n \n Args:\n files: 要检查的文件路径列表,如果为 None 则自动查找输出目录下所有 .py 文件(排除 .gitignore 中的)\n@@ -266,11 +276,13 @@\n tool = self.check_tools[0]\n logger.info(f\"开始并行检查,文件数: {len(files)},工具: {tool}\")\n \n all_results = []\n with ThreadPoolExecutor(max_workers=min(4, len(files))) as executor:\n- futures = [executor.submit(self.run_check, tool, file_path) for file_path in files]\n+ futures = [\n+ executor.submit(self.run_check, tool, file_path) for file_path in files\n+ ]\n \n for future in as_completed(futures):\n try:\n result = future.result()\n all_results.append(result)\n@@ -289,11 +301,13 @@\n json.dump(results, f, indent=2, ensure_ascii=False)\n logger.debug(f\"检查结果已保存至: {self.results_file}\")\n except Exception as e:\n logger.error(f\"保存检查结果失败: {e}\")\n \n- def collect_errors(self, results: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, Any]]:\n+ def collect_errors(\n+ self, results: Optional[List[Dict[str, Any]]] = None\n+ ) -> List[Dict[str, Any]]:\n \"\"\"\n 从检查结果中收集所有错误\n \n Args:\n results: 检查结果列表,如果为 None 则从文件加载\n@@ -316,19 +330,23 @@\n errors = []\n for result in results:\n if result.get(\"errors\") and result[\"errors\"]:\n for error_msg in result[\"errors\"]:\n if error_msg: # 跳过空错误\n- errors.append({\n- \"file\": result[\"file\"],\n- \"tool\": result[\"tool\"],\n- \"error\": error_msg,\n- })\n+ errors.append(\n+ {\n+ \"file\": result[\"file\"],\n+ \"tool\": result[\"tool\"],\n+ \"error\": error_msg,\n+ }\n+ )\n logger.info(f\"收集到 {len(errors)} 个错误\")\n return errors\n \n- def auto_fix(self, errors: List[Dict[str, Any]], context_files: Optional[List[str]] = None) -> bool:\n+ def auto_fix(\n+ self, errors: List[Dict[str, Any]], context_files: Optional[List[str]] = None\n+ ) -> bool:\n \"\"\"\n 自动调用 LLM 生成修复补丁并应用\n \n Args:\n errors: 错误列表,来自 collect_errors\n@@ -365,11 +383,13 @@\n path = Path(file_path)\n if not path.exists():\n path = self.output_dir / file_path\n if path.exists():\n with open(path, \"r\", encoding=\"utf-8\") as f:\n- context_content.append(f\"### 文件: {path.name} (路径: {file_path}) ###\\n{f.read()}\\n\")\n+ context_content.append(\n+ f\"### 文件: {path.name} (路径: {file_path}) ###\\n{f.read()}\\n\"\n+ )\n \n # 添加错误信息\n errors_str = json.dumps(errors, indent=2, ensure_ascii=False)\n context_content.append(f\"### 检查错误列表 ###\\n{errors_str}\\n\")\n \n@@ -384,11 +404,13 @@\n \"注意:只修复提到的错误,保持代码风格一致。\"\n )\n user_prompt = f\"请修复以下检查错误:\\n\\n{full_context}\"\n \n try:\n- result = self.code_generator._call_llm(system_prompt, user_prompt, temperature=0.1)\n+ result = self.code_generator._call_llm(\n+ system_prompt, user_prompt, temperature=0.1\n+ )\n patches = result.get(\"patches\", [])\n description = result.get(\"description\", \"无描述\")\n logger.info(f\"LLM 生成修复补丁: {description}, 补丁数: {len(patches)}\")\n \n # 应用补丁" - ] - }, - { - "tool": "black", - "file": "src/llm_codegen/core.py", - "returncode": 1, - "stdout": "--- src/llm_codegen/core.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/core.py\t2026-03-17 14:57:43.474472+00:00\n@@ -130,46 +130,53 @@\n \"你是一个软件架构师。请根据README描述,生成项目的中间设计文件design.json。\"\n \"design.json应包含项目名称、版本、描述、文件列表(含路径、摘要、依赖、函数和类)、建议命令和检查工具。\"\n \"返回严格的JSON对象,符合DesignModel结构。\"\n )\n user_prompt = f\"README内容如下:\\n\\n{self.readme_content}\"\n- \n+\n result = self._call_llm(system_prompt, user_prompt)\n design_data = result\n design = DesignModel(**design_data)\n- \n+\n # 写入design.json文件\n design_path = self.output_dir / \"design.json\"\n with open(design_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(design.dict(), f, indent=2, ensure_ascii=False)\n logger.info(f\"已生成design.json: {design_path}\")\n- \n+\n return design\n \n def load_state(self) -> Optional[StateModel]:\n \"\"\"加载断点续写状态\"\"\"\n if self.state_file.exists():\n try:\n with open(self.state_file, \"r\", encoding=\"utf-8\") as f:\n state_data = json.load(f)\n self.state = StateModel(**state_data)\n- logger.info(f\"加载状态成功: 当前文件索引 {self.state.current_file_index}\")\n+ logger.info(\n+ f\"加载状态成功: 当前文件索引 {self.state.current_file_index}\"\n+ )\n return self.state\n except Exception as e:\n logger.error(f\"加载状态失败: {e}\")\n return None\n return None\n \n- def save_state(self, current_file_index: int, generated_files: List[str], dependencies_map: Dict[str, List[str]]) -> None:\n+ def save_state(\n+ self,\n+ current_file_index: int,\n+ generated_files: List[str],\n+ dependencies_map: Dict[str, List[str]],\n+ ) -> None:\n \"\"\"保存断点续写状态\"\"\"\n state = StateModel(\n current_file_index=current_file_index,\n generated_files=generated_files,\n dependencies_map=dependencies_map,\n total_files=len(self.design.files) if self.design else 0,\n output_dir=str(self.output_dir),\n- readme_path=self.readme_content[:100] if self.readme_content else \"\"\n+ readme_path=self.readme_content[:100] if self.readme_content else \"\",\n )\n with open(self.state_file, \"w\", encoding=\"utf-8\") as f:\n json.dump(state.dict(), f, indent=2, ensure_ascii=False)\n logger.debug(f\"状态已保存: {self.state_file}\")\n \n@@ -182,18 +189,18 @@\n files: 按顺序需要生成的文件路径列表\n dependencies: 字典 {file: [依赖文件路径]}\n \"\"\"\n if not self.design:\n raise ValueError(\"design.json未加载,请先调用generate_design_json\")\n- \n+\n files = [file.path for file in self.design.files]\n dependencies = {file.path: file.dependencies for file in self.design.files}\n- \n+\n logger.info(f\"从design.json解析到 {len(files)} 个待生成文件\")\n logger.debug(f\"文件列表: {files}\")\n logger.debug(f\"依赖关系: {dependencies}\")\n- \n+\n return files, dependencies\n \n def generate_file(\n self,\n file_path: str,\n@@ -206,18 +213,18 @@\n # 读取依赖文件内容\n context_content = []\n \n if self.readme_content:\n context_content.append(f\"### 项目 README ###\\n{self.readme_content}\\n\")\n- \n+\n # 添加design.json上下文\n design_path = self.output_dir / \"design.json\"\n if design_path.exists():\n with open(design_path, \"r\", encoding=\"utf-8\") as f:\n design_content = f.read()\n context_content.append(f\"### 设计文件: design.json ###\\n{design_content}\\n\")\n- \n+\n for dep in dependency_files:\n dep_path = Path(dep)\n if not dep_path.exists():\n # 尝试相对于当前目录或输出目录查找\n alt_path = self.output_dir / dep\n@@ -226,11 +233,13 @@\n else:\n raise FileNotFoundError(f\"依赖文件不存在: {dep}\")\n \n with open(dep_path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n- context_content.append(f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\")\n+ context_content.append(\n+ f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\"\n+ )\n \n full_context = \"\\n\".join(context_content)\n \n system_prompt = (\n \"你是一个专业的编程助手。根据用户指令和提供的上下文文件,生成完整的代码。\"\n@@ -299,20 +308,24 @@\n self.readme_content = self.parse_readme(readme_path)\n \n # 加载状态\n state = self.load_state()\n if state:\n- console.print(f\"[green]✅ 检测到断点状态,从文件索引 {state.current_file_index} 继续[/green]\")\n+ console.print(\n+ f\"[green]✅ 检测到断点状态,从文件索引 {state.current_file_index} 继续[/green]\"\n+ )\n self.state = state\n # 从状态恢复设计,假设design.json已存在\n design_path = self.output_dir / \"design.json\"\n if design_path.exists():\n with open(design_path, \"r\", encoding=\"utf-8\") as f:\n design_data = json.load(f)\n self.design = DesignModel(**design_data)\n else:\n- console.print(\"[bold yellow]⚠ design.json不存在,重新生成...[/bold yellow]\")\n+ console.print(\n+ \"[bold yellow]⚠ design.json不存在,重新生成...[/bold yellow]\"\n+ )\n self.design = self.generate_design_json()\n else:\n console.print(\"[bold yellow]📋 正在生成设计文件...[/bold yellow]\")\n self.design = self.generate_design_json()\n self.state = None\n@@ -345,11 +358,13 @@\n file_task = progress.add_task(f\"生成 {file}\", total=None)\n \n try:\n # 获取依赖文件\n deps = dependencies.get(file, [])\n- instruction = f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ instruction = (\n+ f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ )\n code, desc, commands = self.generate_file(file, instruction, deps)\n logger.info(f\"生成完成: {file} - {desc}\")\n \n # 写入文件\n output_path = self.output_dir / file\n", - "stderr": "would reformat src/llm_codegen/core.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.\n", - "errors": [ - "would reformat src/llm_codegen/core.py\n\nOh no! 💥 💔 💥\n1 file would be reformatted.", - "--- src/llm_codegen/core.py\t2026-03-17 14:27:38.333874+00:00\n+++ src/llm_codegen/core.py\t2026-03-17 14:57:43.474472+00:00\n@@ -130,46 +130,53 @@\n \"你是一个软件架构师。请根据README描述,生成项目的中间设计文件design.json。\"\n \"design.json应包含项目名称、版本、描述、文件列表(含路径、摘要、依赖、函数和类)、建议命令和检查工具。\"\n \"返回严格的JSON对象,符合DesignModel结构。\"\n )\n user_prompt = f\"README内容如下:\\n\\n{self.readme_content}\"\n- \n+\n result = self._call_llm(system_prompt, user_prompt)\n design_data = result\n design = DesignModel(**design_data)\n- \n+\n # 写入design.json文件\n design_path = self.output_dir / \"design.json\"\n with open(design_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(design.dict(), f, indent=2, ensure_ascii=False)\n logger.info(f\"已生成design.json: {design_path}\")\n- \n+\n return design\n \n def load_state(self) -> Optional[StateModel]:\n \"\"\"加载断点续写状态\"\"\"\n if self.state_file.exists():\n try:\n with open(self.state_file, \"r\", encoding=\"utf-8\") as f:\n state_data = json.load(f)\n self.state = StateModel(**state_data)\n- logger.info(f\"加载状态成功: 当前文件索引 {self.state.current_file_index}\")\n+ logger.info(\n+ f\"加载状态成功: 当前文件索引 {self.state.current_file_index}\"\n+ )\n return self.state\n except Exception as e:\n logger.error(f\"加载状态失败: {e}\")\n return None\n return None\n \n- def save_state(self, current_file_index: int, generated_files: List[str], dependencies_map: Dict[str, List[str]]) -> None:\n+ def save_state(\n+ self,\n+ current_file_index: int,\n+ generated_files: List[str],\n+ dependencies_map: Dict[str, List[str]],\n+ ) -> None:\n \"\"\"保存断点续写状态\"\"\"\n state = StateModel(\n current_file_index=current_file_index,\n generated_files=generated_files,\n dependencies_map=dependencies_map,\n total_files=len(self.design.files) if self.design else 0,\n output_dir=str(self.output_dir),\n- readme_path=self.readme_content[:100] if self.readme_content else \"\"\n+ readme_path=self.readme_content[:100] if self.readme_content else \"\",\n )\n with open(self.state_file, \"w\", encoding=\"utf-8\") as f:\n json.dump(state.dict(), f, indent=2, ensure_ascii=False)\n logger.debug(f\"状态已保存: {self.state_file}\")\n \n@@ -182,18 +189,18 @@\n files: 按顺序需要生成的文件路径列表\n dependencies: 字典 {file: [依赖文件路径]}\n \"\"\"\n if not self.design:\n raise ValueError(\"design.json未加载,请先调用generate_design_json\")\n- \n+\n files = [file.path for file in self.design.files]\n dependencies = {file.path: file.dependencies for file in self.design.files}\n- \n+\n logger.info(f\"从design.json解析到 {len(files)} 个待生成文件\")\n logger.debug(f\"文件列表: {files}\")\n logger.debug(f\"依赖关系: {dependencies}\")\n- \n+\n return files, dependencies\n \n def generate_file(\n self,\n file_path: str,\n@@ -206,18 +213,18 @@\n # 读取依赖文件内容\n context_content = []\n \n if self.readme_content:\n context_content.append(f\"### 项目 README ###\\n{self.readme_content}\\n\")\n- \n+\n # 添加design.json上下文\n design_path = self.output_dir / \"design.json\"\n if design_path.exists():\n with open(design_path, \"r\", encoding=\"utf-8\") as f:\n design_content = f.read()\n context_content.append(f\"### 设计文件: design.json ###\\n{design_content}\\n\")\n- \n+\n for dep in dependency_files:\n dep_path = Path(dep)\n if not dep_path.exists():\n # 尝试相对于当前目录或输出目录查找\n alt_path = self.output_dir / dep\n@@ -226,11 +233,13 @@\n else:\n raise FileNotFoundError(f\"依赖文件不存在: {dep}\")\n \n with open(dep_path, \"r\", encoding=\"utf-8\") as f:\n content = f.read()\n- context_content.append(f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\")\n+ context_content.append(\n+ f\"### 文件: {dep_path.name} (路径: {dep}) ###\\n{content}\\n\"\n+ )\n \n full_context = \"\\n\".join(context_content)\n \n system_prompt = (\n \"你是一个专业的编程助手。根据用户指令和提供的上下文文件,生成完整的代码。\"\n@@ -299,20 +308,24 @@\n self.readme_content = self.parse_readme(readme_path)\n \n # 加载状态\n state = self.load_state()\n if state:\n- console.print(f\"[green]✅ 检测到断点状态,从文件索引 {state.current_file_index} 继续[/green]\")\n+ console.print(\n+ f\"[green]✅ 检测到断点状态,从文件索引 {state.current_file_index} 继续[/green]\"\n+ )\n self.state = state\n # 从状态恢复设计,假设design.json已存在\n design_path = self.output_dir / \"design.json\"\n if design_path.exists():\n with open(design_path, \"r\", encoding=\"utf-8\") as f:\n design_data = json.load(f)\n self.design = DesignModel(**design_data)\n else:\n- console.print(\"[bold yellow]⚠ design.json不存在,重新生成...[/bold yellow]\")\n+ console.print(\n+ \"[bold yellow]⚠ design.json不存在,重新生成...[/bold yellow]\"\n+ )\n self.design = self.generate_design_json()\n else:\n console.print(\"[bold yellow]📋 正在生成设计文件...[/bold yellow]\")\n self.design = self.generate_design_json()\n self.state = None\n@@ -345,11 +358,13 @@\n file_task = progress.add_task(f\"生成 {file}\", total=None)\n \n try:\n # 获取依赖文件\n deps = dependencies.get(file, [])\n- instruction = f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ instruction = (\n+ f\"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。\"\n+ )\n code, desc, commands = self.generate_file(file, instruction, deps)\n logger.info(f\"生成完成: {file} - {desc}\")\n \n # 写入文件\n output_path = self.output_dir / file" - ] - } -] \ No newline at end of file diff --git a/design.json b/design.json index 01b5600..58bade9 100644 --- a/design.json +++ b/design.json @@ -1,186 +1,35 @@ { "project_name": "llm-codegen", - "version": "1.0.0", - "description": "一个基于大语言模型的智能代码生成与维护工具,支持自动生成、增量添加功能和自动修复Bug。", + "version": "0.0.1", + "description": "基于大语言模型的代码生成工具", "files": [ - { - "path": "README.md", - "summary": "项目说明文档,包含项目概述、功能介绍和使用说明", - "dependencies": [ - "src/llm_codegen/cli.py" - ], - "functions": [], - "classes": [], - "design_updates": {} - }, - { - "path": "pyproject.toml", - "summary": "项目元数据、依赖配置和脚本入口", + "path": ".python-version", + "summary": "这是一个Python版本配置文件,指定了项目使用的Python版本为3.13。", "dependencies": [], "functions": [], "classes": [], "design_updates": {} }, { - "path": "src/llm_codegen/__init__.py", - "summary": "包初始化文件", - "dependencies": [ - "src/llm_codegen/core.py" - ], + "path": "README.md", + "summary": "这是一个README.md文件,描述了LLM代码生成工具的项目概述、核心特性、安装方法、使用方式、设计层、工作流、配置和开发指南。它不是一个Python源码文件,而是一个项目文档文件,因此不包含Python代码定义。", + "dependencies": [], "functions": [], "classes": [], "design_updates": {} }, { - "path": "src/llm_codegen/cli.py", - "summary": "命令行接口,使用typer定义命令", - "dependencies": [ - "src/llm_codegen/core.py", - "src/llm_codegen/models.py" - ], - "functions": [ - { - "name": "", - "summary": "", - "inputs": [], - "outputs": [] - } - ], + "path": "pyproject.toml", + "summary": "这是一个Python项目的配置文件(pyproject.toml),定义了项目的构建系统、元数据、依赖项、脚本入口和自定义工具配置。", + "dependencies": [], + "functions": [], "classes": [], "design_updates": {} }, { - "path": "src/llm_codegen/core.py", - "summary": "核心生成逻辑,包含CodeGenerator类", - "dependencies": [ - "src/llm_codegen/utils.py", - "src/llm_codegen/models.py" - ], - "functions": [ - { - "name": "_call_llm", - "summary": "调用LLM并返回解析后的JSON", - "inputs": [ - "system_prompt", - "user_prompt", - "temperature", - "expect_json" - ], - "outputs": [ - "result" - ] - }, - { - "name": "parse_readme", - "summary": "读取README文件内容", - "inputs": [ - "readme_path" - ], - "outputs": [ - "content" - ] - }, - { - "name": "get_project_structure", - "summary": "根据README内容生成文件列表和依赖关系", - "inputs": [], - "outputs": [ - "files", - "dependencies" - ] - }, - { - "name": "generate_file", - "summary": "生成单个文件,返回代码、描述和命令列表", - "inputs": [ - "file_path", - "prompt_instruction", - "dependency_files" - ], - "outputs": [ - "code", - "description", - "commands" - ] - }, - { - "name": "execute_command", - "summary": "执行单个命令,检查风险", - "inputs": [ - "cmd", - "cwd" - ], - "outputs": [] - }, - { - "name": "run", - "summary": "主执行流程,控制整个生成过程", - "inputs": [ - "readme_path" - ], - "outputs": [] - } - ], - "classes": [ - { - "name": "CodeGenerator", - "summary": "代码生成器,封装所有逻辑", - "methods": [ - "__init__", - "_call_llm", - "parse_readme", - "get_project_structure", - "generate_file", - "execute_command", - "run" - ] - } - ], - "design_updates": {} - }, - { - "path": "src/llm_codegen/utils.py", - "summary": "工具函数,如危险命令判断和文件操作", - "dependencies": [ - "src/llm_codegen/models.py" - ], - "functions": [ - { - "name": "is_dangerous_command", - "summary": "判断命令是否危险", - "inputs": [ - "cmd" - ], - "outputs": [ - "is_dangerous", - "reason" - ] - } - ], - "classes": [], - "design_updates": {} - }, - { - "path": "src/llm_codegen/diff_applier.py", - "summary": "", - "dependencies": [ - "src/llm_codegen/models.py" - ], - "functions": [ - { - "name": "", - "summary": "", - "inputs": [], - "outputs": [] - } - ], - "classes": [], - "design_updates": {} - }, - { - "path": "src/llm_codegen/models.py", - "summary": "数据模型,使用Pydantic定义数据结构", + "path": ".gitignore", + "summary": "这是一个.gitignore文件,用于指定Git版本控制系统中应忽略的文件和目录,主要针对Python项目、虚拟环境、日志、缓存和配置文件等。", "dependencies": [], "functions": [], "classes": [], @@ -188,7 +37,7 @@ }, { "path": "tests/__init__.py", - "summary": "测试包初始化", + "summary": "This file initializes the tests package for llm-codegen, allowing it to be recognized as a Python package.", "dependencies": [], "functions": [], "classes": [], @@ -196,56 +45,326 @@ }, { "path": "tests/test_cli.py", - "summary": "测试命令行接口", + "summary": "这是一个测试文件,用于测试基于LLM的自动化代码生成与维护工具的CLI命令,包括init、enhance、fix、check和help命令的成功与失败场景。", "dependencies": [ "src/llm_codegen/cli.py" ], - "functions": [], + "functions": [ + { + "name": "test_cli_init_success", + "summary": "测试init命令成功执行,模拟CodeGenerator并验证输出。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_cli_init_failure_no_readme", + "summary": "测试init命令当README文件不存在时失败。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_cli_enhance_success", + "summary": "测试enhance命令成功执行,模拟CodeGenerator和Path.exists。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_cli_fix_success", + "summary": "测试fix命令成功执行,模拟CodeGenerator和Path.exists。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_cli_help", + "summary": "测试CLI帮助命令,验证帮助文本输出。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_cli_enhance_no_design", + "summary": "测试enhance命令当design.json不存在时失败。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_cli_fix_no_design", + "summary": "测试fix命令当design.json不存在时失败。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_cli_check_success", + "summary": "测试check命令成功执行,模拟CodeGenerator和Checker。", + "inputs": [], + "outputs": [] + } + ], "classes": [], "design_updates": {} }, { - "path": "tests/test_core.py", - "summary": "测试核心生成逻辑", + "path": "tests/test_diff_applier.py", + "summary": "这是一个测试文件,用于测试diff_applier模块中的parse_diff和apply_diff函数,包含多个测试用例验证统一差异解析和应用功能。", "dependencies": [ - "src/llm_codegen/core.py" + "src/llm_codegen/diff_applier.py" + ], + "functions": [ + { + "name": "test_parse_diff", + "summary": "测试解析统一差异字符串以提取文件路径的功能。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_apply_diff_new_file", + "summary": "测试应用创建新文件的差异。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_apply_diff_modify_existing_file", + "summary": "测试应用修改现有文件的差异。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_apply_diff_conflict_handling", + "summary": "测试应用导致冲突的差异处理。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_apply_diff_empty_diff", + "summary": "测试应用空差异字符串。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_apply_diff_invalid_directory", + "summary": "测试应用差异到不存在的目录。", + "inputs": [], + "outputs": [] + }, + { + "name": "test_apply_diff_no_git_repo_initialization", + "summary": "测试应用差异到非git目录。", + "inputs": [], + "outputs": [] + } ], - "functions": [], "classes": [], "design_updates": {} }, { "path": "tests/test_checker.py", - "summary": "测试检查模块", + "summary": "这是一个测试文件,用于测试Checker类的功能,包括检查工具的运行、错误收集和自动修复。它定义了FakeCodeGenerator类作为模拟对象,以及多个pytest fixture和测试类TestChecker,包含各种测试方法。", "dependencies": [ - "src/llm_codegen/checker.py" + "src/llm_codegen/checker.py", + "src/llm_codegen/core.py", + "src/llm_codegen/utils.py" + ], + "functions": [ + { + "name": "fake_code_generator", + "summary": "pytest fixture,返回一个假的CodeGenerator实例用于测试。", + "inputs": [], + "outputs": [ + "FakeCodeGenerator实例" + ] + }, + { + "name": "checker", + "summary": "pytest fixture,创建Checker实例,使用临时目录和假的code_generator。", + "inputs": [ + "fake_code_generator", + "tmp_path" + ], + "outputs": [ + "Checker实例" + ] + } + ], + "classes": [ + { + "name": "FakeCodeGenerator", + "summary": "假的CodeGenerator类,用于替代真实的LLM调用,模拟_call_llm方法。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FakeCodeGenerator实例,设置_call_llm调用标志和参数。", + "inputs": [ + "self", + "return_value" + ], + "outputs": [] + }, + { + "name": "_call_llm", + "summary": "模拟_call_llm方法,记录调用参数并返回预设值。", + "inputs": [ + "self", + "system_prompt", + "user_prompt", + "temperature" + ], + "outputs": [ + "return_value" + ] + } + ] + }, + { + "name": "TestChecker", + "summary": "测试类,包含多个测试方法,用于验证Checker类的各种功能。", + "methods": [ + { + "name": "test_init", + "summary": "测试Checker的初始化方法,验证属性设置。", + "inputs": [ + "self", + "checker", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_run_check_success", + "summary": "测试run_check方法成功运行检查工具的情况。", + "inputs": [ + "self", + "checker", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_run_check_timeout", + "summary": "测试run_check方法处理超时的情况。", + "inputs": [ + "self", + "checker", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_run_parallel_checks", + "summary": "测试并行运行检查工具的方法。", + "inputs": [ + "self", + "checker", + "tmp_path", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_save_results", + "summary": "测试保存检查结果到文件的方法。", + "inputs": [ + "self", + "checker", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_collect_errors", + "summary": "测试从结果中收集错误的方法。", + "inputs": [ + "self", + "checker", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_collect_errors_no_results", + "summary": "测试无结果文件时收集错误的方法。", + "inputs": [ + "self", + "checker" + ], + "outputs": [] + }, + { + "name": "test_auto_fix", + "summary": "测试自动修复错误的方法。", + "inputs": [ + "self", + "checker", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_auto_fix_no_errors", + "summary": "测试无错误时自动修复的方法。", + "inputs": [ + "self", + "checker" + ], + "outputs": [] + }, + { + "name": "test_run_full_check_and_fix", + "summary": "测试完整检查与修复循环成功的情况。", + "inputs": [ + "self", + "checker", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_run_full_check_and_fix_failure", + "summary": "测试完整检查与修复循环失败的情况。", + "inputs": [ + "self", + "checker", + "monkeypatch" + ], + "outputs": [] + } + ] + } ], - "functions": [], - "classes": [], - "design_updates": {} - }, - { - "path": "README.md", - "summary": "自动生成的新文件", - "dependencies": [], - "functions": [], - "classes": [], "design_updates": {} }, { "path": "src/llm_codegen/init_generator.py", - "summary": "初始化命令生成器,处理 init 命令逻辑", + "summary": "处理 init 命令的生成器类,继承自 CodeGenerator,用于从 README 初始化项目。", "dependencies": [ "src/llm_codegen/core.py", - "src/llm_codegen/models.py" + "src/llm_codegen/design_generator.py", + "src/llm_codegen/design_manager.py" ], "functions": [], "classes": [ { "name": "InitGenerator", - "summary": "继承自 BaseGenerator,包含 run 方法", + "summary": "处理 init 命令的生成器类,继承自 CodeGenerator,用于从 README 初始化项目。", "methods": [ - "run" + { + "name": "__init__", + "summary": "初始化 InitGenerator,设置 API 参数、输出目录等,并创建 DesignGenerator 实例。", + "inputs": [ + "api_key: Optional[str]", + "base_url: str", + "model: str", + "output_dir: str", + "max_concurrency: int" + ], + "outputs": [] + }, + { + "name": "run", + "summary": "处理 init 命令逻辑:根据 README.md 初始化项目,包括读取 README、生成设计文件、排序文件、生成代码、执行命令等步骤。", + "inputs": [ + "readme_path: Path" + ], + "outputs": [] + } ] } ], @@ -253,26 +372,49 @@ }, { "path": "src/llm_codegen/enhance_generator.py", - "summary": "增强命令生成器,处理 enhance 命令逻辑", + "summary": "定义了一个增强生成器类EnhanceGenerator,继承自CodeGenerator,专门处理enhance命令逻辑,用于根据需求工单对现有项目进行功能增强,包括读取工单、分析变更、生成或修改文件、更新设计等操作。", "dependencies": [ "src/llm_codegen/core.py", - "src/llm_codegen/models.py" + "src/llm_codegen/design_manager.py" ], "functions": [], "classes": [ { "name": "EnhanceGenerator", - "summary": "继承自 BaseGenerator,包含 process_enhance 方法", + "summary": "增强生成器类,继承自CodeGenerator,专门处理enhance命令逻辑,用于根据需求工单对现有项目进行功能增强。", "methods": [ - "process_enhance" + { + "name": "__init__", + "summary": "初始化EnhanceGenerator,继承BaseGenerator的配置,设置API密钥、基础URL、模型、输出目录、日志文件和最大并发数。", + "inputs": [ + "api_key", + "base_url", + "model", + "output_dir", + "log_file", + "max_concurrency" + ], + "outputs": [] + }, + { + "name": "process_enhance", + "summary": "处理enhance命令的核心逻辑:读取工单,分析变更,生成或修改文件,更新设计。", + "inputs": [ + "issue_file_path", + "output_format" + ], + "outputs": [ + "bool" + ] + } ] } ], "design_updates": {} }, { - "path": "src/llm_codegen/fix_generator.py", - "summary": "修复命令生成器,处理 fix 命令逻辑", + "path": "tests/test_core.py", + "summary": "这是一个测试文件,用于测试CodeGenerator类及其相关功能,包括模拟OpenAI客户端、测试初始化、解析README、生成设计JSON、生成文件、执行命令、断点续写和拓扑排序等功能。", "dependencies": [ "src/llm_codegen/core.py", "src/llm_codegen/models.py" @@ -280,10 +422,265 @@ "functions": [], "classes": [ { - "name": "FixGenerator", - "summary": "继承自 BaseGenerator,包含 process_fix 方法", + "name": "FakeChatCompletion", + "summary": "模拟OpenAI的chat.completions.create返回值,包含choices属性。", "methods": [ - "process_fix" + { + "name": "__init__", + "summary": "初始化FakeChatCompletion实例,设置choices属性。", + "inputs": [ + "self", + "content" + ], + "outputs": [] + } + ] + }, + { + "name": "FakeChoice", + "summary": "模拟OpenAI的choice对象,包含message属性。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FakeChoice实例,设置message属性。", + "inputs": [ + "self", + "message" + ], + "outputs": [] + } + ] + }, + { + "name": "FakeMessage", + "summary": "模拟OpenAI的message对象,包含content和reasoning_content属性。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FakeMessage实例,设置content和reasoning_content属性。", + "inputs": [ + "self", + "content" + ], + "outputs": [] + } + ] + }, + { + "name": "FakeOpenAIClient", + "summary": "假的OpenAI客户端,用于替换真实客户端,包含chat属性。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FakeOpenAIClient实例,设置chat属性为FakeChat实例。", + "inputs": [ + "self" + ], + "outputs": [] + } + ] + }, + { + "name": "FakeChat", + "summary": "模拟OpenAI的chat对象,包含completions属性。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FakeChat实例,设置completions属性为FakeCompletions实例。", + "inputs": [ + "self" + ], + "outputs": [] + } + ] + }, + { + "name": "FakeCompletions", + "summary": "模拟OpenAI的completions对象,包含create方法,用于记录调用和返回模拟值。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FakeCompletions实例,设置create_called、create_kwargs和create_return_value属性。", + "inputs": [ + "self" + ], + "outputs": [] + }, + { + "name": "create", + "summary": "模拟OpenAI的create方法,记录调用参数并返回FakeChatCompletion实例。", + "inputs": [ + "self", + "*args", + "**kwargs" + ], + "outputs": [ + "FakeChatCompletion" + ] + } + ] + }, + { + "name": "TestCodeGenerator", + "summary": "测试CodeGenerator类的测试类,包含多个测试方法,覆盖初始化、文件解析、设计生成、命令执行、断点续写和拓扑排序等功能。", + "methods": [ + { + "name": "test_init_success", + "summary": "测试CodeGenerator初始化成功,验证API密钥、模型、输出目录和客户端替换。", + "inputs": [ + "self", + "code_generator", + "tmp_path", + "fake_openai_client" + ], + "outputs": [] + }, + { + "name": "test_init_no_api_key", + "summary": "测试没有API密钥时CodeGenerator初始化抛出ValueError。", + "inputs": [ + "self", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_parse_readme_success", + "summary": "测试解析README文件成功,返回文件内容。", + "inputs": [ + "self", + "code_generator", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_parse_readme_file_not_found", + "summary": "测试README文件不存在时解析抛出异常。", + "inputs": [ + "self", + "code_generator" + ], + "outputs": [] + }, + { + "name": "test_generate_design_json", + "summary": "测试生成design.json文件,模拟LLM调用并验证设计模型和文件写入。", + "inputs": [ + "self", + "code_generator", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_generate_file_with_dependencies", + "summary": "测试生成文件,包含依赖文件,模拟LLM调用并验证返回的代码、描述和命令。", + "inputs": [ + "self", + "code_generator", + "monkeypatch", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_execute_command_success", + "summary": "测试执行命令成功,模拟subprocess.run返回成功。", + "inputs": [ + "self", + "code_generator", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_execute_command_dangerous", + "summary": "测试阻止危险命令执行,模拟is_dangerous_command返回危险。", + "inputs": [ + "self", + "code_generator", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_execute_command_failure", + "summary": "测试命令执行失败,模拟subprocess.run返回失败。", + "inputs": [ + "self", + "code_generator", + "monkeypatch" + ], + "outputs": [] + }, + { + "name": "test_run_with_state_resume", + "summary": "测试断点续写功能,模拟状态文件存在时的运行流程。", + "inputs": [ + "self", + "code_generator", + "monkeypatch", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_run_without_state", + "summary": "测试没有状态文件时的首次运行,模拟完整流程。", + "inputs": [ + "self", + "code_generator", + "monkeypatch", + "tmp_path" + ], + "outputs": [] + }, + { + "name": "test_topological_sort_normal", + "summary": "测试拓扑排序正常依赖排序,验证排序顺序正确。", + "inputs": [ + "self", + "code_generator" + ], + "outputs": [] + }, + { + "name": "test_topological_sort_cycle_detection", + "summary": "测试拓扑排序循环依赖检测,验证抛出ValueError。", + "inputs": [ + "self", + "code_generator" + ], + "outputs": [] + }, + { + "name": "test_topological_sort_empty", + "summary": "测试拓扑排序空输入,验证返回空列表。", + "inputs": [ + "self", + "code_generator" + ], + "outputs": [] + }, + { + "name": "test_topological_sort_partial_deps", + "summary": "测试拓扑排序部分依赖不在列表中,验证忽略不在列表中的依赖。", + "inputs": [ + "self", + "code_generator" + ], + "outputs": [] + }, + { + "name": "test_topological_sort_complex", + "summary": "测试拓扑排序复杂依赖关系,验证排序满足所有依赖。", + "inputs": [ + "self", + "code_generator" + ], + "outputs": [] + } ] } ], @@ -291,29 +688,1343 @@ }, { "path": "src/llm_codegen/design_generator.py", - "summary": "自动生成的新文件", + "summary": "该文件是LLM代码生成工具的一部分,包含一个设计生成器类,用于处理设计文件的生成、增量更新和源代码分析,包括模拟git忽略文件的功能。", + "dependencies": [ + "./core", + "./models" + ], + "functions": [ + { + "name": "get_non_ignored_files", + "summary": "模拟git行为,递归遍历目录并应用.gitignore规则,返回所有未被忽略的文件。", + "inputs": [ + "root_path: Path" + ], + "outputs": [ + "result_files: List[Path]" + ] + }, + { + "name": "is_ignored", + "summary": "判断一个文件是否被.gitignore规则忽略,通过合并所有祖先目录的规则进行匹配。", + "inputs": [ + "file_path: Path", + "root_path: Path", + "specs: dict" + ], + "outputs": [ + "bool" + ] + } + ], + "classes": [ + { + "name": "DesignGenerator", + "summary": "设计生成器类,继承自CodeGenerator,专门处理设计文件的生成、增量更新和源代码分析。", + "methods": [ + { + "name": "__init__", + "summary": "初始化设计生成器,设置API密钥、输出目录等参数。", + "inputs": [ + "api_key: Optional[str]", + "base_url: str", + "model: str", + "output_dir: str", + "max_concurrency: int" + ], + "outputs": [] + }, + { + "name": "process_design_command", + "summary": "处理设计命令,基于工单内容实现设计文件的生成和增量更新逻辑。", + "inputs": [ + "issue_content: str", + "issue_type: str" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "_analyze_single_file", + "summary": "分析单个文件并返回设计信息字典,包括摘要、依赖、函数和类。", + "inputs": [ + "file_path: Path", + "root_dir: Path" + ], + "outputs": [ + "Dict[str, Any]" + ] + }, + { + "name": "analyze_source_files", + "summary": "分析源代码目录以提取设计信息,用于生成或刷新design.json。", + "inputs": [ + "source_dir: Path" + ], + "outputs": [ + "Dict[str, Any]" + ] + }, + { + "name": "refresh_design_from_source", + "summary": "从源代码目录刷新design.json,基于分析结果。", + "inputs": [ + "source_dir: Path" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "update_single_file_design", + "summary": "更新design.json中单个文件的条目,基于文件内容。", + "inputs": [ + "file_path: Path" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "run", + "summary": "主执行流程,用于集成到命令行接口,支持从README生成、处理工单或更新单个文件。", + "inputs": [ + "readme_path: Optional[Path]", + "issue_content: Optional[str]", + "update_file: Optional[Path]" + ], + "outputs": [] + } + ] + } + ], + "design_updates": {} + }, + { + "path": "src/llm_codegen/__init__.py", + "summary": "llm-codegen包的初始化文件,定义包版本和描述,并导出核心模块以便从包级别导入。", "dependencies": [], "functions": [], "classes": [], "design_updates": {} }, { - "path": "tests/test_design_generator.py", - "summary": "自动生成的新文件", + "path": "src/llm_codegen/models.py", + "summary": "该文件定义了多个Pydantic模型,用于代码生成工具中的数据结构,包括设计模型、工单模型、状态模型和响应模型。", "dependencies": [], "functions": [], + "classes": [ + { + "name": "FileStatus", + "summary": "文件生成状态枚举,定义pending、generating、success和failed四种状态。", + "methods": [] + }, + { + "name": "OutputFormat", + "summary": "输出格式枚举,定义full和diff两种格式。", + "methods": [] + }, + { + "name": "FunctionModel", + "summary": "函数模型,对应design.json中的functions字段,用于表示函数的结构。", + "methods": [] + }, + { + "name": "ClassModel", + "summary": "类模型,对应design.json中的classes字段,用于表示类的结构。", + "methods": [] + }, + { + "name": "FileModel", + "summary": "文件模型,对应design.json中的files字段,用于表示文件的结构,并包含合并设计更新的方法。", + "methods": [ + { + "name": "merge_design_updates", + "summary": "合并设计更新到当前文件模型。", + "inputs": [ + "updates" + ], + "outputs": [] + } + ] + }, + { + "name": "DesignModel", + "summary": "设计模型,对应design.json的根结构,用于表示整个项目的设计信息。", + "methods": [] + }, + { + "name": "FeatureIssue", + "summary": "需求工单模型,基于README中的模板,用于表示需求工单的结构。", + "methods": [] + }, + { + "name": "BugIssue", + "summary": "Bug工单模型,基于README中的模板,用于表示Bug工单的结构。", + "methods": [] + }, + { + "name": "StateModel", + "summary": "状态模型,用于保存生成过程中的断点状态。", + "methods": [] + }, + { + "name": "LLMResponse", + "summary": "LLM响应模型,用于解析generate_file方法的返回,表示LLM生成的代码和相关信息。", + "methods": [] + } + ], + "design_updates": {} + }, + { + "path": "src/llm_codegen/fix_generator.py", + "summary": "定义了一个处理Bug修复逻辑的生成器类FixGenerator,继承自CodeGenerator,用于读取Bug工单、分析变更、生成并应用修复代码。", + "dependencies": [ + "src/llm_codegen/core.py", + "src/llm_codegen/models.py", + "src/llm_codegen/design_manager.py" + ], + "functions": [], + "classes": [ + { + "name": "FixGenerator", + "summary": "处理Bug修复逻辑的生成器类,继承自CodeGenerator,用于处理fix命令,包括读取Bug工单、分析变更、生成修复代码并应用。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FixGenerator,继承基类参数。", + "inputs": [ + "**kwargs" + ], + "outputs": [] + }, + { + "name": "process_fix", + "summary": "处理fix命令逻辑:读取Bug工单文件,分析变更,生成并应用修复代码,支持不同输出格式。", + "inputs": [ + "bug_issue_path", + "output_format" + ], + "outputs": [ + "bool", + "List[Path]" + ] + } + ] + } + ], + "design_updates": {} + }, + { + "path": "src/llm_codegen/cli.py", + "summary": "LLM代码生成工具的命令行接口,使用typer构建CLI,支持init、enhance、fix、design四种操作模式,用于自动化代码生成与维护。", + "dependencies": [ + "src/llm_codegen/init_generator.py", + "src/llm_codegen/enhance_generator.py", + "src/llm_codegen/fix_generator.py", + "src/llm_codegen/design_generator.py" + ], + "functions": [ + { + "name": "init_logging", + "summary": "初始化日志配置,将日志输出到logs/目录,支持控制台和文件记录。", + "inputs": [ + "output_dir: Path", + "log_file: Optional[str] = None", + "command_name: str = \"cli\"" + ], + "outputs": [ + "str" + ] + } + ], "classes": [], "design_updates": {} + }, + { + "path": "src/llm_codegen/design_manager.py", + "summary": "设计管理类,用于处理 design.json 文件的加载、保存、README 哈希计算、校验和同步功能。", + "dependencies": [ + "src/llm_codegen/models.py", + "src/llm_codegen/utils.py" + ], + "functions": [], + "classes": [ + { + "name": "DesignManager", + "summary": "设计管理类,用于处理 design.json 文件的加载、保存、README 哈希计算、校验和同步功能。", + "methods": [ + { + "name": "__init__", + "summary": "初始化 DesignManager,设置可选的 design.json 文件路径。", + "inputs": [ + "design_file_path: Optional[Path] = None" + ], + "outputs": [] + }, + { + "name": "load_design", + "summary": "加载 design.json 文件并解析为 DesignModel 对象。", + "inputs": [ + "file_path: Optional[Path] = None" + ], + "outputs": [ + "DesignModel" + ] + }, + { + "name": "save_design", + "summary": "将 DesignModel 对象保存为 design.json 文件。", + "inputs": [ + "design: DesignModel", + "file_path: Optional[Path] = None" + ], + "outputs": [] + }, + { + "name": "compute_readme_hash", + "summary": "计算 README 文件的 SHA256 哈希值。", + "inputs": [ + "readme_path: Path" + ], + "outputs": [ + "str" + ] + }, + { + "name": "validate_readme_hash", + "summary": "校验 README 文件的哈希值与 design.json 中存储的哈希值是否一致。", + "inputs": [ + "design: DesignModel", + "readme_path: Optional[Path] = None" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "sync_with_readme", + "summary": "同步 design.json 与 README 文件,更新 readme_path 和 readme_hash。", + "inputs": [ + "design: DesignModel", + "readme_path: Path" + ], + "outputs": [ + "DesignModel" + ] + } + ] + } + ], + "design_updates": {} + }, + { + "path": "src/llm_codegen/core.py", + "summary": "该文件定义了一个代码生成器基类CodeGenerator,用于基于LLM生成代码文件,支持设计层管理、断点续写、并发生成、依赖分析和命令执行等功能。", + "dependencies": [ + ".llm_client", + ".utils", + ".models", + ".design_manager" + ], + "functions": [], + "classes": [ + { + "name": "CodeGenerator", + "summary": "代码生成器基类,封装公共逻辑,支持设计层、断点续写和命令执行", + "methods": [ + { + "name": "__init__", + "summary": "初始化生成器,设置API客户端、输出目录和状态文件", + "inputs": [ + "api_key: Optional[str]", + "base_url: str", + "model: str", + "output_dir: Path", + "max_concurrency: int" + ], + "outputs": [] + }, + { + "name": "_call_llm", + "summary": "调用LLM客户端生成响应,支持系统提示和用户提示", + "inputs": [ + "system_prompt: str", + "user_prompt: str", + "temperature: float", + "expect_json: bool" + ], + "outputs": [ + "result" + ] + }, + { + "name": "parse_readme", + "summary": "读取README文件内容并返回", + "inputs": [ + "readme_path: Path" + ], + "outputs": [ + "content: str" + ] + }, + { + "name": "load_state", + "summary": "加载断点续写状态文件,返回状态模型", + "inputs": [], + "outputs": [ + "state: Optional[StateModel]" + ] + }, + { + "name": "save_state", + "summary": "保存断点续写状态,适应并发生成(线程安全)", + "inputs": [ + "generated_files: List[str]", + "dependencies_map: Dict[str, List[str]]" + ], + "outputs": [] + }, + { + "name": "get_project_structure", + "summary": "从design.json获取文件列表和依赖关系", + "inputs": [], + "outputs": [ + "files: List[str]", + "dependencies: Dict[str, List[str]]" + ] + }, + { + "name": "_add_implicit_dependencies", + "summary": "添加隐式依赖关系,基于文件路径和常见模式", + "inputs": [ + "files: List[str]", + "dependencies: Dict[str, List[str]]" + ], + "outputs": [ + "enhanced: Dict[str, List[str]]" + ] + }, + { + "name": "generate_file", + "summary": "生成单个文件,返回代码、描述和命令列表", + "inputs": [ + "file_path: str", + "prompt_instruction: str", + "dependency_files: List[str]", + "existing_content: Optional[str]", + "output_format: str" + ], + "outputs": [ + "code: str", + "description: str", + "commands: List[str]" + ] + }, + { + "name": "_generate_file_task", + "summary": "并发任务函数,用于生成单个文件", + "inputs": [ + "file_path: str", + "dependencies: List[str]", + "generated_files: set" + ], + "outputs": [ + "success: bool", + "error_message: str" + ] + }, + { + "name": "_topological_sort", + "summary": "对文件列表进行拓扑排序,基于依赖关系", + "inputs": [ + "files: List[str]", + "dependencies: Dict[str, List[str]]" + ], + "outputs": [ + "sorted_files: List[str]" + ] + }, + { + "name": "execute_command", + "summary": "执行单个命令,检查风险,失败仅记录错误不抛出异常", + "inputs": [ + "cmd: str", + "cwd: Optional[Path]" + ], + "outputs": [ + "success: bool" + ] + }, + { + "name": "_analyze_issue", + "summary": "调用LLM分析工单,返回结构化变更计划", + "inputs": [ + "issue_content: str", + "issue_type: str" + ], + "outputs": [ + "result: Dict[str, Any]" + ] + }, + { + "name": "sync_readme", + "summary": "同步README.md和design.json,利用哈希值判断一致性并更新", + "inputs": [], + "outputs": [ + "success: bool" + ] + } + ] + } + ], + "design_updates": {} + }, + { + "path": "src/llm_codegen/llm_client.py", + "summary": "LLM客户端类,用于与LLM模型交互,管理API调用、重试逻辑和响应记录保存。", + "dependencies": [], + "functions": [], + "classes": [ + { + "name": "LLMClient", + "summary": "LLM客户端,负责与模型交互并管理响应记录。", + "methods": [ + { + "name": "__init__", + "summary": "初始化LLM客户端,设置API参数、输出目录和重试配置。", + "inputs": [ + "api_key", + "base_url", + "model", + "output_dir", + "max_retries", + "retry_delay" + ], + "outputs": [] + }, + { + "name": "call", + "summary": "调用LLM并返回解析后的结果,支持JSON输出和重试机制。", + "inputs": [ + "messages", + "temperature", + "expect_json" + ], + "outputs": [ + "Dict[str, Any]" + ] + }, + { + "name": "_save_response", + "summary": "将LLM响应保存到文件中,包括消息、内容和元数据。", + "inputs": [ + "messages", + "content", + "reasoning_content", + "temperature", + "expect_json" + ], + "outputs": [] + } + ] + } + ], + "design_updates": {} + }, + { + "path": "src/llm_codegen/utils.py", + "summary": "该文件是LLM代码生成项目中的一个工具模块,提供了一系列实用函数和类,包括命令安全检查、文件操作、路径处理、错误日志记录、依赖图构建和拓扑排序、并发队列管理以及进度条创建等功能。", + "dependencies": [], + "functions": [ + { + "name": "is_dangerous_command", + "summary": "判断命令字符串是否包含危险关键词,返回布尔值和原因。", + "inputs": [ + "cmd" + ], + "outputs": [ + "bool", + "str" + ] + }, + { + "name": "read_file", + "summary": "读取指定文件路径的内容,返回文件内容字符串,失败时抛出IOError。", + "inputs": [ + "file_path" + ], + "outputs": [ + "str" + ] + }, + { + "name": "write_file", + "summary": "将内容写入指定文件路径,确保目录存在,失败时抛出IOError。", + "inputs": [ + "file_path", + "content" + ], + "outputs": [] + }, + { + "name": "ensure_dir", + "summary": "确保指定目录存在,如果不存在则创建。", + "inputs": [ + "directory" + ], + "outputs": [] + }, + { + "name": "safe_join", + "summary": "安全地拼接基础路径和多个部分,防止目录遍历攻击,返回绝对路径。", + "inputs": [ + "base_path", + "*paths" + ], + "outputs": [ + "str" + ] + }, + { + "name": "log_error", + "summary": "记录和显示错误信息,根据是否致命使用不同日志级别。", + "inputs": [ + "error", + "message", + "is_fatal" + ], + "outputs": [] + }, + { + "name": "is_fatal_error", + "summary": "判断异常对象是否为致命错误类型(如SystemExit、KeyboardInterrupt等)。", + "inputs": [ + "error" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "build_dependency_graph", + "summary": "基于文件列表构建依赖图,返回邻接表表示的字典。", + "inputs": [ + "files" + ], + "outputs": [ + "Dict[str, List[str]]" + ] + }, + { + "name": "compute_in_degrees", + "summary": "计算依赖图中每个节点的入度值,返回字典。", + "inputs": [ + "graph" + ], + "outputs": [ + "Dict[str, int]" + ] + }, + { + "name": "add_implicit_dependency", + "summary": "根据文件内容检查并添加隐式依赖到依赖列表,返回更新后的列表。", + "inputs": [ + "file_content", + "current_deps", + "implicit_dep_file" + ], + "outputs": [ + "List[str]" + ] + }, + { + "name": "create_progress_bar", + "summary": "创建并配置一个标准的rich进度条实例,用于任务进度显示。", + "inputs": [ + "total", + "description", + "columns", + "auto_refresh" + ], + "outputs": [ + "Progress" + ] + }, + { + "name": "topological_sort", + "summary": "基于依赖图进行拓扑排序,检测循环依赖并报错,返回排序后的节点列表。", + "inputs": [ + "graph" + ], + "outputs": [ + "List[str]" + ] + } + ], + "classes": [ + { + "name": "ConcurrentQueueManager", + "summary": "管理并发队列的简单类,用于并行任务如代码生成或检查,提供队列操作功能。", + "methods": [ + { + "name": "__init__", + "summary": "初始化队列,设置最大大小。", + "inputs": [ + "maxsize" + ], + "outputs": [] + }, + { + "name": "enqueue", + "summary": "将项目加入队列。", + "inputs": [ + "item" + ], + "outputs": [] + }, + { + "name": "dequeue", + "summary": "从队列中取出项目,可设置阻塞和超时。", + "inputs": [ + "block", + "timeout" + ], + "outputs": [ + "Any" + ] + }, + { + "name": "is_empty", + "summary": "检查队列是否为空。", + "inputs": [], + "outputs": [ + "bool" + ] + }, + { + "name": "size", + "summary": "获取队列中项目数量。", + "inputs": [], + "outputs": [ + "int" + ] + } + ] + } + ], + "design_updates": {} + }, + { + "path": "design.json", + "summary": "这是一个名为 llm-codegen 的基于大语言模型的代码生成工具的设计文件,描述了项目的整体结构、文件依赖关系、函数和类定义,用于自动化代码生成与维护。", + "dependencies": [ + "src/llm_codegen/init_generator.py", + "src/llm_codegen/design_generator.py", + "src/llm_codegen/enhance_generator.py", + "src/llm_codegen/models.py", + "src/llm_codegen/__init__.py", + "src/llm_codegen/fix_generator.py", + "src/llm_codegen/core.py", + "src/llm_codegen/cli.py", + "src/llm_codegen/utils.py" + ], + "functions": [ + { + "name": "is_dangerous_command", + "summary": "检查命令是否危险,从utils模块导入,用于execute_command方法中验证命令安全性。", + "inputs": [ + "cmd: str" + ], + "outputs": [ + "bool", + "str" + ] + }, + { + "name": "init_logging", + "summary": "初始化日志配置到logs/目录,设置控制台和文件日志处理器。", + "inputs": [ + "output_dir: Path", + "log_file: Optional[str]", + "command_name: str" + ], + "outputs": [ + "str" + ] + }, + { + "name": "is_dangerous_command", + "summary": "判断命令字符串是否包含危险关键词。", + "inputs": [ + "cmd" + ], + "outputs": [ + "Tuple[bool, str]" + ] + }, + { + "name": "read_file", + "summary": "读取指定文件路径的内容。", + "inputs": [ + "file_path" + ], + "outputs": [ + "str" + ] + }, + { + "name": "write_file", + "summary": "将内容写入指定文件路径,自动创建父目录。", + "inputs": [ + "file_path", + "content" + ], + "outputs": [ + "None" + ] + }, + { + "name": "ensure_dir", + "summary": "确保指定目录存在,如果不存在则创建。", + "inputs": [ + "directory" + ], + "outputs": [ + "None" + ] + }, + { + "name": "safe_join", + "summary": "安全地拼接路径,防止目录遍历攻击。", + "inputs": [ + "base_path", + "*paths" + ], + "outputs": [ + "str" + ] + }, + { + "name": "log_error", + "summary": "记录和显示错误信息,支持自定义消息和致命错误标记。", + "inputs": [ + "error", + "message", + "is_fatal" + ], + "outputs": [ + "None" + ] + }, + { + "name": "is_fatal_error", + "summary": "判断异常是否为致命错误类型。", + "inputs": [ + "error" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "build_dependency_graph", + "summary": "基于文件列表构建依赖图。", + "inputs": [ + "files" + ], + "outputs": [ + "Dict[str, List[str]]" + ] + }, + { + "name": "compute_in_degrees", + "summary": "计算依赖图中每个节点的入度。", + "inputs": [ + "graph" + ], + "outputs": [ + "Dict[str, int]" + ] + }, + { + "name": "add_implicit_dependency", + "summary": "根据文件内容添加隐式依赖。", + "inputs": [ + "file_content", + "current_deps", + "implicit_dep_file" + ], + "outputs": [ + "List[str]" + ] + }, + { + "name": "create_progress_bar", + "summary": "创建并配置一个标准的rich进度条。", + "inputs": [ + "total", + "description", + "columns", + "auto_refresh" + ], + "outputs": [ + "Progress" + ] + }, + { + "name": "topological_sort", + "summary": "基于依赖图进行拓扑排序,检测循环依赖。", + "inputs": [ + "graph" + ], + "outputs": [ + "List[str]" + ] + } + ], + "classes": [ + { + "name": "InitGenerator", + "summary": "处理 init 命令的生成器类,继承自 CodeGenerator,用于从 README 初始化项目。", + "methods": [ + { + "name": "__init__", + "summary": "初始化 InitGenerator,设置 API 密钥、基础 URL、模型、输出目录、日志文件和最大并发数。", + "inputs": [ + "api_key: Optional[str]", + "base_url: str", + "model: str", + "output_dir: str", + "log_file: Optional[str]", + "max_concurrency: int" + ], + "outputs": [] + }, + { + "name": "run", + "summary": "处理 init 命令逻辑:根据 README.md 初始化项目,包括读取 README、生成设计文件、获取项目结构、拓扑排序、生成文件、执行命令和保存状态。", + "inputs": [ + "readme_path: Path" + ], + "outputs": [] + } + ] + }, + { + "name": "DesignGenerator", + "summary": "设计生成器类,负责处理设计文件的生成、更新和源代码分析。", + "methods": [ + { + "name": "__init__", + "summary": "初始化设计生成器,设置API密钥、输出目录等参数,并继承父类初始化。", + "inputs": [ + "api_key: Optional[str]", + "base_url: str", + "model: str", + "output_dir: str", + "log_file: Optional[str]", + "max_concurrency: int" + ], + "outputs": [] + }, + { + "name": "process_design_command", + "summary": "处理设计命令,基于工单内容实现设计文件的生成和增量更新,包括源文件分析、LLM调用和合并/覆盖处理。", + "inputs": [ + "issue_content: str", + "issue_type: str" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "analyze_source_files", + "summary": "分析源代码目录以提取设计信息,用于生成或刷新design.json,通过调用LLM分析每个Python文件。", + "inputs": [ + "source_dir: Path" + ], + "outputs": [ + "Dict[str, Any]" + ] + }, + { + "name": "refresh_design_from_source", + "summary": "从源代码目录刷新design.json,基于analyze_source_files的分析结果构建并保存DesignModel。", + "inputs": [ + "source_dir: Path" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "run", + "summary": "主执行流程,用于集成到命令行接口,支持从README生成design.json或处理工单内容。", + "inputs": [ + "readme_path: Optional[Path]", + "issue_content: Optional[str]" + ], + "outputs": [] + } + ] + }, + { + "name": "EnhanceGenerator", + "summary": "增强生成器类,继承自BaseGenerator,专门处理enhance命令逻辑,用于根据需求工单(feature.issue)对现有项目进行功能增强。", + "methods": [ + { + "name": "__init__", + "summary": "初始化EnhanceGenerator,继承BaseGenerator的配置。", + "inputs": [ + "api_key: Optional[str]", + "base_url: str", + "model: str", + "output_dir: str", + "log_file: Optional[str]", + "max_concurrency: int" + ], + "outputs": [] + }, + { + "name": "process_enhance", + "summary": "处理enhance命令的核心逻辑:读取工单,分析变更,生成或修改文件,更新设计。", + "inputs": [ + "issue_file_path: Path", + "output_format: str" + ], + "outputs": [ + "bool" + ] + } + ] + }, + { + "name": "FileStatus", + "summary": "文件生成状态枚举,定义了pending、generating、success和failed四种状态。", + "methods": [] + }, + { + "name": "OutputFormat", + "summary": "输出格式枚举,定义了full和diff两种格式。", + "methods": [] + }, + { + "name": "FunctionModel", + "summary": "函数模型,对应design.json中的functions字段,用于表示函数信息。", + "methods": [] + }, + { + "name": "ClassModel", + "summary": "类模型,对应design.json中的classes字段,用于表示类信息。", + "methods": [] + }, + { + "name": "FileModel", + "summary": "文件模型,对应design.json中的files字段,用于表示文件信息,并包含合并设计更新的方法。", + "methods": [ + { + "name": "merge_design_updates", + "summary": "合并设计更新到当前文件模型。", + "inputs": [ + "updates" + ], + "outputs": [] + } + ] + }, + { + "name": "DesignModel", + "summary": "设计模型,对应design.json的根结构,用于表示整个项目的设计信息。", + "methods": [] + }, + { + "name": "FeatureIssue", + "summary": "需求工单模型,基于README中的模板,用于表示功能需求工单。", + "methods": [] + }, + { + "name": "BugIssue", + "summary": "Bug工单模型,基于README中的模板,用于表示Bug报告工单。", + "methods": [] + }, + { + "name": "StateModel", + "summary": "状态模型,用于保存生成过程中的断点状态,如文件索引和状态。", + "methods": [] + }, + { + "name": "LLMResponse", + "summary": "LLM响应模型,用于解析generate_file方法的返回,包含代码、描述和输出格式等信息。", + "methods": [] + }, + { + "name": "FixGenerator", + "summary": "处理Bug修复逻辑的生成器类,继承自CodeGenerator,用于处理fix命令,包括读取Bug工单、分析变更、生成并应用修复代码。", + "methods": [ + { + "name": "__init__", + "summary": "初始化FixGenerator,继承基类参数。", + "inputs": [ + "**kwargs" + ], + "outputs": [] + }, + { + "name": "process_fix", + "summary": "处理fix命令逻辑:读取Bug工单,分析变更,生成并应用修复代码。", + "inputs": [ + "bug_issue_path: Path", + "output_format: OutputFormat = OutputFormat.FULL" + ], + "outputs": [ + "bool" + ] + } + ] + }, + { + "name": "CodeGenerator", + "summary": "代码生成器基类,封装公共逻辑,支持设计层、断点续写和命令执行。", + "methods": [ + { + "name": "__init__", + "summary": "初始化生成器,设置API客户端、输出目录、日志和状态。", + "inputs": [ + "api_key: Optional[str]", + "base_url: str", + "model: str", + "output_dir: str", + "log_file: Optional[str]", + "max_concurrency: int" + ], + "outputs": [] + }, + { + "name": "_call_llm", + "summary": "调用LLM并返回解析后的JSON,保存响应到文件。", + "inputs": [ + "system_prompt: str", + "user_prompt: str", + "temperature: float", + "expect_json: bool" + ], + "outputs": [ + "Dict[str, Any]" + ] + }, + { + "name": "parse_readme", + "summary": "读取README文件内容。", + "inputs": [ + "readme_path: Path" + ], + "outputs": [ + "str" + ] + }, + { + "name": "generate_design_json", + "summary": "调用LLM生成design.json内容,并解析为DesignModel。", + "inputs": [], + "outputs": [ + "DesignModel" + ] + }, + { + "name": "load_state", + "summary": "加载断点续写状态。", + "inputs": [], + "outputs": [ + "Optional[StateModel]" + ] + }, + { + "name": "save_state", + "summary": "保存断点续写状态,适应并发生成(线程安全)。", + "inputs": [ + "generated_files: List[str]", + "dependencies_map: Dict[str, List[str]]" + ], + "outputs": [] + }, + { + "name": "get_project_structure", + "summary": "从design.json获取文件列表和依赖关系。", + "inputs": [], + "outputs": [ + "Tuple[List[str], Dict[str, List[str]]]" + ] + }, + { + "name": "_add_implicit_dependencies", + "summary": "添加隐式依赖关系,基于文件路径和常见模式。", + "inputs": [ + "files: List[str]", + "dependencies: Dict[str, List[str]]" + ], + "outputs": [ + "Dict[str, List[str]]" + ] + }, + { + "name": "generate_file", + "summary": "生成单个文件,返回代码、描述和命令列表。", + "inputs": [ + "file_path: str", + "prompt_instruction: str", + "dependency_files: List[str]", + "existing_content: Optional[str]", + "output_format: str" + ], + "outputs": [ + "Tuple[str, str, List[str]]" + ] + }, + { + "name": "_generate_file_task", + "summary": "并发任务函数,用于生成单个文件。", + "inputs": [ + "file_path: str", + "dependencies: List[str]", + "generated_files: set" + ], + "outputs": [ + "Tuple[bool, str]" + ] + }, + { + "name": "_topological_sort", + "summary": "对文件列表进行拓扑排序,基于依赖关系。", + "inputs": [ + "files: List[str]", + "dependencies: Dict[str, List[str]]" + ], + "outputs": [ + "List[str]" + ] + }, + { + "name": "execute_command", + "summary": "执行单个命令,检查风险,失败仅记录错误不抛出异常。", + "inputs": [ + "cmd: str", + "cwd: Optional[Path]" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "_analyze_issue", + "summary": "调用LLM分析工单,返回结构化变更计划。", + "inputs": [ + "issue_content: str", + "issue_type: str" + ], + "outputs": [ + "Dict[str, Any]" + ] + }, + { + "name": "_update_design", + "summary": "根据生成的变更更新design.json。", + "inputs": [ + "generated_files: List[str]", + "design_updates: Dict[str, Any]" + ], + "outputs": [] + }, + { + "name": "refresh_design", + "summary": "重新生成design.json,基于当前README内容或加载的design.json。", + "inputs": [], + "outputs": [ + "bool" + ] + }, + { + "name": "update_file_entry", + "summary": "更新design.json中单个文件的条目,基于提供的文件内容。", + "inputs": [ + "file_path: str", + "file_content: str" + ], + "outputs": [ + "bool" + ] + }, + { + "name": "sync_readme", + "summary": "同步README.md和design.json,确保内容一致性。", + "inputs": [], + "outputs": [ + "bool" + ] + } + ] + }, + { + "name": "ConcurrentQueueManager", + "summary": "管理并发队列的简单类,用于并行任务如代码生成或检查。", + "methods": [ + { + "name": "__init__", + "summary": "初始化队列。", + "inputs": [ + "maxsize" + ], + "outputs": [ + "None" + ] + }, + { + "name": "enqueue", + "summary": "将项目加入队列。", + "inputs": [ + "item" + ], + "outputs": [ + "None" + ] + }, + { + "name": "dequeue", + "summary": "从队列中取出项目。", + "inputs": [ + "block", + "timeout" + ], + "outputs": [ + "Any" + ] + }, + { + "name": "is_empty", + "summary": "检查队列是否为空。", + "inputs": [], + "outputs": [ + "bool" + ] + }, + { + "name": "size", + "summary": "获取队列中项目数量。", + "inputs": [], + "outputs": [ + "int" + ] + } + ] + } + ], + "design_updates": {} } ], - "commands": [ - "pip install -e .", - "pytest tests/" - ], - "check_tools": [ - "pytest", - "pylint", - "mypy", - "black" - ] + "commands": [], + "check_tools": [], + "readme_path": "README.md", + "readme_hash": "5d0832d556d6aaf64718fc8d29516653fe61f550fb52e3cd924e91ec1979c84a" } \ No newline at end of file diff --git a/llmcodegen.py b/llmcodegen.py deleted file mode 100644 index 9674f8c..0000000 --- a/llmcodegen.py +++ /dev/null @@ -1,360 +0,0 @@ -#!/home/songsenand/env/.venv/bin/python -#! -""" -基于LLM的自动化代码生成工具 -根据README.md文件,自动生成项目文件结构并填充代码,执行必要命令。 -""" - -import json -import os -import subprocess -import sys -from typing import List, Dict, Optional, Any, Tuple -from pathlib import Path - -import typer -from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskID -from loguru import logger -from openai import OpenAI - -# ==================== 配置 ==================== -DANGEROUS_COMMANDS = ["rm", "sudo", "chmod", "dd", "mkfs", "> /dev/sda", "format"] -ALLOWED_COMMANDS = [] # 可设置白名单,为空则只检查黑名单 - -app = typer.Typer(help="基于LLM的自动化代码生成工具") -console = Console() - -# ==================== 工具函数 ==================== -def is_dangerous_command(cmd: str) -> Tuple[bool, str]: - """ - 判断命令是否危险 - 返回 (是否危险, 原因) - """ - cmd_lower = cmd.lower() - for danger in DANGEROUS_COMMANDS: - if danger in cmd_lower: - return True, f"包含危险关键词 '{danger}'" - return False, "" - -# ==================== 核心类 ==================== -class CodeGenerator: - """代码生成器,封装所有逻辑""" - - def __init__( - self, - api_key: Optional[str] = None, - base_url: str = "https://api.deepseek.com", - model: str = "deepseek-reasoner", - output_dir: str = "./generated", - log_file: Optional[str] = None, - ): - """ - 初始化生成器 - - Args: - api_key: OpenAI API密钥,默认从环境变量DEEPSEEK_APIKEY读取 - base_url: API基础URL - model: 使用的模型 - output_dir: 输出根目录 - log_file: 日志文件路径,默认自动生成 - """ - self.api_key = api_key or os.getenv("DEEPSEEK_APIKEY") - if not self.api_key: - raise ValueError("必须提供API密钥,或设置环境变量DEEPSEEK_APIKEY") - - self.client = OpenAI(api_key=self.api_key, base_url=base_url) - self.model = model - self.output_dir = Path(output_dir) - self.output_dir.mkdir(parents=True, exist_ok=True) - - # 配置日志 - if log_file is None: - log_file = self.output_dir / "generator.log" - logger.remove() # 移除默认handler - logger.add(sys.stderr, level="WARNING") # 控制台输出INFO及以上 - logger.add(log_file, rotation="10 MB", level="DEBUG") # 文件记录DEBUG - logger.info(f"日志已初始化,保存至: {log_file}") - - self.readme_content = None - - self.progress: Optional[Progress] = None - self.tasks: Dict[str, TaskID] = {} # 任务ID映射 - - def _call_llm( - self, - system_prompt: str, - user_prompt: str, - temperature: float = 0.2, - expect_json: bool = True, - ) -> Dict[str, Any]: - """ - 调用LLM并返回解析后的JSON - """ - logger.debug(f"调用LLM,模型: {self.model}") - logger.debug(f"System: {system_prompt[:200]}...") - logger.debug(f"User: {user_prompt[:200]}...") - - try: - response = self.client.chat.completions.create( - model=self.model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], - temperature=temperature, - response_format={"type": "json_object"} if expect_json else None, - ) - - message = response.choices[0].message - content = message.content - - # 记录思考过程(如果存在) - if hasattr(message, "reasoning_content") and message.reasoning_content: - logger.info(f"模型思考过程: {message.reasoning_content}") - - logger.debug(f"LLM原始响应: {content[:500]}...") - - if expect_json: - result = json.loads(content) - else: - result = {"content": content} - - return result - - except json.JSONDecodeError as e: - logger.error(f"JSON解析失败: {e}") - raise ValueError(f"LLM返回的不是有效JSON: {content[:200]}") - except Exception as e: - logger.error(f"LLM调用失败: {e}") - raise - - def parse_readme(self, readme_path: Path) -> str: - """ - 读取README文件内容 - """ - logger.info(f"读取README文件: {readme_path}") - try: - with open(readme_path, "r", encoding="utf-8") as f: - content = f.read() - logger.debug(f"README内容长度: {len(content)} 字符") - if (readme_path.parent / 'design.json').exists(): - with open((readme_path.parent / 'design.json')) as f: - content += f'\n\ndesign.json(包含项目设计有关信息)内容如下:{f.read()}' - return content - except Exception as e: - logger.error(f"读取README失败: {e}") - raise - - def get_project_structure(self) -> Tuple[List[str], Dict[str, List[str]]]: - """ - 根据README内容,让LLM生成文件列表和依赖关系 - - Returns: - (files, dependencies) - files: 按顺序需要生成的文件路径列表 - dependencies: 字典 {file: [依赖文件路径]} - """ - system_prompt = ( - "你是一个软件架构师。请根据README描述,分析需要生成哪些源代码文件,并确定它们的生成顺序," - "同时给出每个文件生成时最少需要读取哪些已有文件作为上下文。" - "返回严格的JSON对象,包含两个字段:\n" - "- files: 数组,按生成顺序排列的文件路径(相对于项目根目录)\n" - "- dependencies: 对象,键为文件路径,值为该文件依赖的已有文件路径列表(可为空)\n" - "注意:依赖文件必须是已存在的参考文件,不要包含待生成的文件。" - ) - user_prompt = f"README内容如下:\n\n{self.readme_content}" - - result = self._call_llm(system_prompt, user_prompt) - - files = result.get("files", []) - dependencies = result.get("dependencies", {}) - - if not files: - raise ValueError("LLM未返回任何文件列表") - - logger.info(f"解析到 {len(files)} 个待生成文件") - logger.debug(f"文件列表: {files}") - logger.debug(f"依赖关系: {dependencies}") - - return files, dependencies - - def generate_file( - self, - file_path: str, - prompt_instruction: str, - dependency_files: List[str], - ) -> Tuple[str, str, List[str]]: - """ - 生成单个文件,返回 (代码, 描述, 命令列表) - """ - # 读取依赖文件内容 - context_content = [] - - if self.readme_content: - context_content.append(f"### 项目 README ###\n{self.readme_content}\n") - - for dep in dependency_files: - dep_path = Path(dep) - if not dep_path.exists(): - # 尝试相对于当前目录或输出目录查找 - alt_path = self.output_dir / dep - if alt_path.exists(): - dep_path = alt_path - else: - logger.warning(FileNotFoundError(f"依赖文件不存在: {dep}")) - - with open(dep_path, "r", encoding="utf-8") as f: - content = f.read() - context_content.append(f"### 文件: {dep_path.name} (路径: {dep}) ###\n{content}\n") - - full_context = "\n".join(context_content) - - system_prompt = ( - "你是一个专业的编程助手。根据用户指令和提供的上下文文件,生成完整的代码。" - "返回严格的JSON对象,包含三个字段:\n" - "- code: (string) 生成的完整代码\n" - "- description: (string) 简短的中文功能描述\n" - "- commands: (array of string) 生成此文件后需要执行的操作系统命令列表(如编译、安装依赖等),若无则返回空数组" - ) - user_prompt = f"{prompt_instruction}\n\n参考文件上下文:\n{full_context}" - - result = self._call_llm(system_prompt, user_prompt) - - code = result.get("code", "") - description = result.get("description", "") - commands = result.get("commands", []) - - if not isinstance(commands, list): - commands = [] - - return code, description, commands - - def execute_command(self, cmd: str, cwd: Optional[Path] = None) -> None: - """ - 执行单个命令,检查风险 - """ - dangerous, reason = is_dangerous_command(cmd) - if dangerous: - logger.error(f"危险命令被阻止: {cmd},原因: {reason}") - return - - logger.info(f"执行命令: {cmd}") - try: - result = subprocess.run( - cmd, - shell=True, - cwd=cwd or self.output_dir, - capture_output=True, - text=True, - timeout=300, # 5分钟超时 - ) - logger.debug(f"命令返回码: {result.returncode}") - if result.stdout: - logger.debug(f"stdout: {result.stdout[:500]}") - if result.stderr: - logger.warning(f"stderr: {result.stderr[:500]}") - except subprocess.TimeoutExpired: - logger.error(f"命令执行超时: {cmd}") - except Exception as e: - logger.error(f"命令执行失败: {e}") - - - def run(self, readme_path: Path): - """ - 主执行流程 - """ - logger.info("=" * 50) - logger.info("开始代码生成流程") - logger.info(f"README: {readme_path}") - logger.info(f"输出目录: {self.output_dir}") - - # 初始化阶段:用rich输出状态(不会被日志级别过滤) - console.print("[bold yellow]🔍 正在解析README...[/bold yellow]") - self.readme_content = self.parse_readme(readme_path) - - console.print("[bold yellow]📋 正在分析项目结构...[/bold yellow]") - files, dependencies = self.get_project_structure() - - console.print(f"[green]✅ 解析完成,共 {len(files)} 个文件待生成[/green]") - - # 3. 创建进度条 - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - console=console, - ) as progress: - self.progress = progress - # 创建总任务 - total_task = progress.add_task("[cyan]整体进度...", total=len(files)) - - # 依次生成每个文件 - for idx, file in enumerate(files, 1): - logger.info(f"处理文件 [{idx}/{len(files)}]: {file}") - - # 创建子任务(可选) - file_task = progress.add_task(f"生成 {file}", total=None) - - try: - # 获取依赖文件 - deps = dependencies.get(file, []) - - # 构造生成指令 - instruction = f"请根据README描述和依赖文件,生成文件 '{file}' 的完整代码。" - - # 调用LLM生成代码 - code, desc, commands = self.generate_file(file, instruction, deps) - - logger.info(f"生成完成: {file} - {desc}") - - # 写入文件 - output_path = self.output_dir / file - output_path.parent.mkdir(parents=True, exist_ok=True) - with open(output_path, "w", encoding="utf-8") as f: - f.write(code) - logger.info(f"已写入: {output_path}") - - # 执行命令 - for cmd in commands: - logger.info(f"准备执行命令: {cmd}") - self.execute_command(cmd, cwd=self.output_dir) - - except Exception as e: - logger.error(f"处理文件 {file} 失败: {e}") - # 可选:继续或终止 - finally: - progress.remove_task(file_task) - progress.update(total_task, advance=1) - - logger.success("所有文件处理完成!") - -# ==================== CLI入口 ==================== -@app.command() -def main( - 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="输出根目录,默认为readme所在目录"), - api_key: Optional[str] = typer.Option(None, "--api-key", envvar="DEEPSEEK_APIKEY", help="API密钥,也可通过环境变量DEEPSEEK_APIKEY设置"), - 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="日志文件路径(默认输出目录下generator.log)"), -): - """ - 根据README自动生成项目代码 - """ - if output_dir is None: - output_dir = readme.parent - - generator = CodeGenerator( - api_key=api_key, - base_url=base_url, - model=model, - output_dir=output_dir, - log_file=log_file, - ) - generator.run(readme) - - -if __name__ == "__main__": - app() diff --git a/pyproject.toml b/pyproject.toml index cc62f5e..ab30960 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ default=true llm-codegen = "llm_codegen.cli:app" [tool.llm-codegen] -check_tools = ["pytest", "pylint", "mypy", "black"] +check_tools = ["black"] max_retries = 3 dangerous_commands = ["rm", "sudo", "chmod", "dd"] max_concurrent_requests = 5 diff --git a/src/llm_codegen/cli.py b/src/llm_codegen/cli.py index 9172bde..7d67eb6 100644 --- a/src/llm_codegen/cli.py +++ b/src/llm_codegen/cli.py @@ -5,15 +5,15 @@ LLM 代码生成工具的命令行接口 """ from pathlib import Path -from typing import Optional +from typing import Optional, List import sys +import traceback import typer from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn from loguru import logger -from .core import BaseGenerator from .init_generator import InitGenerator from .enhance_generator import EnhanceGenerator from .fix_generator import FixGenerator @@ -63,7 +63,7 @@ def init( output_dir = Path.cwd() # 初始化日志配置 - log_file_path = init_logging(output_dir, log_file, command_name="init") + init_logging(output_dir, log_file, command_name="init") # 处理致命错误:检查README文件存在性(已由typer处理),其他错误在try块中捕获 try: @@ -79,7 +79,6 @@ def init( base_url=base_url, model=model, output_dir=str(output_dir), - log_file=log_file_path, max_concurrency=max_concurrency, ) generator.run(readme) @@ -120,7 +119,7 @@ def enhance( output_dir = Path.cwd() # 初始化日志配置 - log_file_path = init_logging(output_dir, log_file, command_name="enhance") + init_logging(output_dir, log_file, command_name="enhance") # 处理致命错误:检查design.json是否存在 design_path = output_dir / "design.json" @@ -142,12 +141,20 @@ def enhance( 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") + success, affected_files = generator.process_enhance(issue_file, output_format="full") if success: progress.update(task_id, completed=1, description="增强处理完成") # 修改:成功时更新完成状态 + generator = DesignGenerator( + api_key=api_key, + base_url=base_url, + model=model, + output_dir=str(output_dir), + max_concurrency=max_concurrency, + ) + for file_path in affected_files: + generator.update_single_file_design(file_path) else: progress.update(task_id, description="增强处理失败") # 修改:失败时更新描述 if not success: @@ -174,7 +181,7 @@ def fix( base_url: str = typer.Option( "https://api.deepseek.com", "--base-url", help="API基础URL" ), - model: str = typer.Option("deepseek-reasoner", "--model", "-m", help="使用的模型"), + model: str = typer.Option("deepseek-chat", "--model", "-m", help="使用的模型"), log_file: Optional[str] = typer.Option(None, "--log", help="日志文件路径"), max_concurrency: int = typer.Option( 4, "--max-concurrency", help="并发生成的最大工作线程数,默认4" @@ -185,7 +192,7 @@ def fix( output_dir = Path.cwd() # 初始化日志配置 - log_file_path = init_logging(output_dir, log_file, command_name="fix") + init_logging(output_dir, log_file, command_name="fix") # 处理致命错误:检查design.json是否存在 design_path = output_dir / "design.json" @@ -206,12 +213,20 @@ def fix( 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") + (success, affected_files) = generator.process_fix(issue_file, output_format="full") if success: progress.update(task_id, completed=1, description="修复处理完成") # 修改:成功时更新完成状态 + generator = DesignGenerator( + api_key=api_key, + base_url=base_url, + model=model, + output_dir=str(output_dir), + max_concurrency=max_concurrency, + ) + for file_path in affected_files: + generator.update_single_file_design(file_path) else: progress.update(task_id, description="修复处理失败") # 修改:失败时更新描述 if not success: @@ -242,6 +257,14 @@ def design( file_okay=False, dir_okay=True, ), + single_files: Optional[List[Path]] = typer.Option( + None, + "--single-file", + help="指定单个或多个文件路径来更新design.json中的条目,可以是相对或绝对路径;支持多次使用以指定多个文件,保持向后兼容全量生成行为", + exists=True, + file_okay=True, + dir_okay=False, + ), output_dir: Optional[Path] = typer.Option( None, "--output", "-o", help="输出目录,design.json将保存在此,默认为当前目录" ), @@ -253,22 +276,22 @@ def design( base_url: str = typer.Option( "https://api.deepseek.com", "--base-url", help="API基础URL" ), - model: str = typer.Option("deepseek-reasoner", "--model", "-m", help="使用的模型"), + model: str = typer.Option("deepseek-chat", "--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生成、从源代码刷新,并集成新的设计生成逻辑。""" + """生成或更新design.json:支持从README生成、从源代码刷新,并集成新的设计生成逻辑,新增单个或多个文件更新功能。""" if output_dir is None: output_dir = Path.cwd() # 初始化日志配置 - log_file_path = init_logging(output_dir, log_file, command_name="design") + init_logging(output_dir, log_file, command_name="design") # 检查是否提供了至少一个操作参数 - if file is None and source is None: - logger.error("必须提供 --file 或 --source 参数之一来执行操作。") + if file is None and source is None and not single_files: + logger.error("必须提供 --file、--source 或 --single-file 参数之一来执行操作。") raise typer.Exit(code=1) # 初始化DesignGenerator以集成新的设计生成逻辑 @@ -285,7 +308,6 @@ def design( base_url=base_url, model=model, output_dir=str(output_dir), - log_file=log_file_path, max_concurrency=max_concurrency, ) @@ -294,6 +316,9 @@ def design( # 处理--dry-run选项 if dry_run: console.print("[yellow]模拟运行模式:不会实际写入文件或执行命令。[/yellow]") + if single_files: + for f in single_files: + console.print(f"[blue]模拟:将更新文件 {f} 在design.json中的条目[/blue]") if file is not None: logger.info(f"模拟:将从README文件 {file} 生成design.json") # 在dry-run模式下,仅模拟解析README @@ -309,6 +334,19 @@ def design( return # 实际运行逻辑 + if single_files: + # 处理单个或多个文件更新 + logger.info(f"开始处理文件更新,共 {len(single_files)} 个文件") + for f in single_files: + success = generator.update_single_file_design(f) + if not success: + logger.error(f"更新文件 {f} 失败") + raise typer.Exit(code=1) + console.print("[green]✅ 所有指定文件已更新design.json[/green]") + progress.update(task_id, completed=1, description="文件更新完成") + return # 处理完文件更新后退出,不执行其他全量生成逻辑 + + # 全量生成行为(保持向后兼容) if file is not None: # 生成design.json从README if not force and design_path.exists(): @@ -330,9 +368,10 @@ def design( 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}") + error_message = traceback.format_exc() + logger.error(f"处理design命令失败: {e}, 堆栈跟踪: {error_message}") raise typer.Exit(code=1) if __name__ == "__main__": - app() \ No newline at end of file + app() diff --git a/src/llm_codegen/core.py b/src/llm_codegen/core.py index 95d569d..ba26e44 100644 --- a/src/llm_codegen/core.py +++ b/src/llm_codegen/core.py @@ -1,26 +1,23 @@ import json import os import subprocess -import sys -import concurrent.futures -import pendulum from typing import List, Dict, Optional, Any, Tuple from pathlib import Path -from collections import deque import threading +import traceback from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskID +from rich.progress import Progress, TaskID from loguru import logger -from openai import OpenAI +from .llm_client import LLMClient from .utils import is_dangerous_command from .models import ( DesignModel, StateModel, FileModel, - FileStatus, -) # 添加 FileStatus 导入 +) +from .design_manager import DesignManager # 添加导入 class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 @@ -31,8 +28,7 @@ class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 api_key: Optional[str] = None, base_url: str = "https://api.deepseek.com", model: str = "deepseek-reasoner", - output_dir: str = "./generated", - log_file: Optional[str] = None, + output_dir: Path = Path("./generated"), max_concurrency: int = 4, ): """ @@ -49,9 +45,10 @@ class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 if not self.api_key: raise ValueError("必须提供API密钥,或设置环境变量DEEPSEEK_APIKEY") - self.client = OpenAI(api_key=self.api_key, base_url=base_url) - self.model = model - self.output_dir = Path(output_dir) + self.client = LLMClient(self.api_key, base_url, model, output_dir) + if isinstance(output_dir, str): + output_dir = Path(output_dir) + self.output_dir = output_dir self.output_dir.mkdir(parents=True, exist_ok=True) self.state_file = self.output_dir / ".llm_generator_state.json" self.console = Console() # 添加console实例用于rich打印 @@ -59,14 +56,6 @@ class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 self.max_concurrency = max_concurrency - # 配置日志 - if log_file is None: - log_file = self.output_dir / "generator.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}") - self.readme_content = None self.design: Optional[DesignModel] = None self.state: Optional[StateModel] = None @@ -79,72 +68,15 @@ class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 user_prompt: str, temperature: float = 0.2, expect_json: bool = True, - ) -> Dict[str, Any]: - """ - 调用LLM并返回解析后的JSON - """ - logger.debug(f"调用LLM,模型: {self.model}") - - try: - response = self.client.chat.completions.create( - model=self.model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], - temperature=temperature, - response_format={"type": "json_object"} if expect_json else None, - ) - - message = response.choices[0].message - content = message.content - - # 记录思考过程(如果存在) - reasoning_content = None - if hasattr(message, "reasoning_content") and message.reasoning_content: - reasoning_content = message.reasoning_content - logger.info("模型思考过程已记录") - - # 创建响应目录 - responses_dir = self.output_dir / "llm_responses" - responses_dir.mkdir(parents=True, exist_ok=True) - - # 生成文件名(使用当前时间) - timestamp = pendulum.now().format("YYYYMMDD_HHmmss_SSS") - response_file = responses_dir / f"response_{timestamp}.json" - - # 保存响应到JSON文件 - response_data = { - "timestamp": timestamp, - "model": self.model, - "content": content, - "reasoning_content": reasoning_content, - "system_prompt": system_prompt, - "user_prompt": user_prompt, - "temperature": temperature, - "expect_json": expect_json, - } - - with open(response_file, "w", encoding="utf-8") as f: - json.dump(response_data, f, indent=2, ensure_ascii=False) - - logger.debug(f"LLM原始响应: {response_file.name}") - - if expect_json: - result = json.loads(content) - else: - result = {"content": content} - - return result - - except json.JSONDecodeError as e: - logger.error(f"JSON解析失败: {e}") - self.console.print(f"[bold red]❌ JSON解析失败: {e}[/bold red]") - raise ValueError(f"LLM返回的不是有效JSON: {content[:200]}") - except Exception as e: - logger.error(f"LLM调用失败: {e}") - self.console.print(f"[bold red]❌ LLM调用失败: {e}[/bold red]") - raise + ): + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + result = self.client.call( + messages=messages, temperature=temperature, expect_json=expect_json + ) + return result def parse_readme(self, readme_path: Path) -> str: """ @@ -161,28 +93,6 @@ class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 self.console.print(f"[bold red]❌ 读取README失败: {e}[/bold red]") raise - def generate_design_json(self) -> DesignModel: - """ - 调用LLM生成design.json内容,并解析为DesignModel - """ - system_prompt = ( - "你是一个软件架构师。请根据README描述,生成项目的中间设计文件design.json。" - "design.json应包含项目名称、版本、描述、文件列表(含路径、摘要、依赖、函数和类)、建议命令和检查工具。" - "返回严格的 JSON 对象,符合DesignModel结构。" - ) - user_prompt = f"README内容如下:\n\n{self.readme_content}" - - result = self._call_llm(system_prompt, user_prompt) - design_data = result - design = DesignModel(**design_data) - - # 写入design.json文件 - design_path = self.output_dir / "design.json" - with open(design_path, "w", encoding="utf-8") as f: - json.dump(design.model_dump(), f, indent=2, ensure_ascii=False) - logger.info(f"已生成design.json: {design_path}") - - return design def load_state(self) -> Optional[StateModel]: """加载断点续写状态""" @@ -364,9 +274,15 @@ class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 ) user_prompt = f"{prompt_instruction}\n\n参考文件上下文:\n{full_context}" + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] try: - result = self._call_llm(system_prompt, user_prompt) + result = self.client.call( + messages=messages, temperature=0.2, expect_json=True + ) code = result.get("code") description = result.get("description", "") commands = result.get("commands", []) @@ -546,275 +462,54 @@ class CodeGenerator: # 修改为 CodeGenerator 以符合设计文件 f"工单内容:\n{issue_content}\n\n" f"现有设计文件 (design.json):\n{design_str}" ) - - result = self._call_llm(system_prompt, user_prompt, temperature=0.2) + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + result = self.client.call(messages, expect_json=True) return result - def _update_design( - self, generated_files: List[str], design_updates: Dict[str, Any] - ): - """ - 根据生成的变更更新 design.json - 使用 FileModel 来处理文件信息 - """ - updated = False - - # 处理新增文件 - for file_path in generated_files: - # 检查文件是否已在 design.files 中 - exists = any(f.path == file_path for f in self.design.files) - if not exists: - # 获取更新信息 - update_info = design_updates.get(file_path, {}) - - # 创建新文件条目(FileModel实例) - new_file = FileModel( - path=file_path, - summary=update_info.get("summary", "自动生成的新文件"), - dependencies=update_info.get("dependencies", []), - functions=update_info.get("functions", []), - classes=update_info.get("classes", []), - design_updates=update_info.get("design_updates", {}), - ) - self.design.files.append(new_file) - updated = True - logger.info(f"已将新文件 {file_path} 添加到 design.json") - - # 如果 design_updates 中提供了具体的更新信息,可以进一步处理(例如修改现有文件的摘要) - # 这里可根据实际需求扩展,当前仅处理新增文件 - - if updated: - # 保存更新后的 design.json - design_path = self.output_dir / "design.json" - with open(design_path, "w", encoding="utf-8") as f: - json.dump(self.design.model_dump(), f, indent=2, ensure_ascii=False) - logger.info("design.json 已更新") - - def refresh_design(self) -> bool: - """ - 重新生成design.json,基于当前README内容或加载的design.json - 返回bool表示是否成功 - """ - logger.info("开始刷新design.json") - if not self.readme_content: - # 尝试读取README.md文件 - readme_path = self.output_dir / "README.md" - if readme_path.exists(): - try: - self.readme_content = self.parse_readme(readme_path) - except Exception as e: - logger.error(f"读取README.md失败,无法刷新design: {e}") - self.console.print( - f"[bold red]❌ 读取README.md失败,无法刷新design: {e}[/bold red]" - ) - return False - else: - logger.error("没有README内容,且README.md文件不存在,无法刷新design") - self.console.print( - "[bold red]❌ 没有README内容,且README.md文件不存在,无法刷新design[/bold red]" - ) - return False - - try: - self.design = self.generate_design_json() - logger.info("design.json已成功重新生成") - self.console.print("[green]✅ design.json已重新生成[/green]") - return True - except Exception as e: - logger.error(f"重新生成design.json失败: {e}") - self.console.print(f"[bold red]❌ 重新生成design.json失败: {e}[/bold red]") - return False - - def update_file_entry(self, file_path: str, file_content: str) -> bool: - """ - 更新design.json中单个文件的条目,基于提供的文件内容 - 返回bool表示是否成功 - """ - logger.info(f"开始更新design.json中文件条目: {file_path}") - if not self.design: - # 加载现有design.json - design_path = self.output_dir / "design.json" - if not design_path.exists(): - logger.error(f"design.json不存在于 {self.output_dir}") - self.console.print( - f"[bold red]❌ design.json不存在于 {self.output_dir}[/bold red]" - ) - return False - try: - with open(design_path, "r", encoding="utf-8") as f: - design_data = json.load(f) - self.design = DesignModel(**design_data) - except Exception as e: - logger.error(f"加载design.json失败: {e}") - self.console.print(f"[bold red]❌ 加载design.json失败: {e}[/bold red]") - return False - - # 调用LLM分析文件内容,返回更新信息,增强以支持design_updates字段 - system_prompt = ( - "你是一个软件架构师。分析给定的文件内容,并返回对design.json中该文件条目的更新。" - "返回严格的JSON对象,包含以下字段:\n" - "- summary: 文件的新摘要\n" - "- dependencies: 依赖文件列表\n" - "- functions: 函数列表,每个对象有name, summary, inputs, outputs\n" - "- classes: 类列表,每个对象有name, summary, methods\n" - "- design_updates: 可选,设计更新字典\n" - "注意:仅返回JSON,不要其他文本。" - ) - # 准备当前design.json中该文件的条目信息 - current_entry = None - for f in self.design.files: - if f.path == file_path: - current_entry = f.model_dump() - break - user_prompt = f"文件路径: {file_path}\n文件内容:\n{file_content}\n\n当前design.json中该文件的条目(如果存在):\n{json.dumps(current_entry, indent=2) if current_entry else '无'}" - - try: - result = self._call_llm(system_prompt, user_prompt, temperature=0.2) - update_info = result - - # 查找或创建文件条目 - file_model = None - for f in self.design.files: - if f.path == file_path: - file_model = f - break - if file_model is None: - # 创建新条目,包括design_updates - new_file = FileModel( - path=file_path, - summary=update_info.get("summary", ""), - dependencies=update_info.get("dependencies", []), - functions=update_info.get("functions", []), - classes=update_info.get("classes", []), - design_updates=update_info.get("design_updates", {}), # 新增design_updates处理 - ) - self.design.files.append(new_file) - logger.info(f"在design.json中创建了新文件条目: {file_path}") - else: - # 更新现有条目,使用merge_design_updates处理design_updates - if 'design_updates' in update_info: - file_model.merge_design_updates(update_info['design_updates']) - # 更新其他字段 - file_model.summary = update_info.get("summary", file_model.summary) - file_model.dependencies = update_info.get( - "dependencies", file_model.dependencies - ) - file_model.functions = update_info.get( - "functions", file_model.functions - ) - file_model.classes = update_info.get("classes", file_model.classes) - logger.info(f"更新了design.json中的文件条目: {file_path}") - - # 保存更新后的design.json - design_path = self.output_dir / "design.json" - with open(design_path, "w", encoding="utf-8") as f: - json.dump(self.design.model_dump(), f, indent=2, ensure_ascii=False) - logger.info(f"design.json已更新,文件条目: {file_path}") - self.console.print( - f"[green]✅ design.json中文件条目 {file_path} 已更新[/green]" - ) - return True - except Exception as e: - logger.error(f"更新文件条目失败: {e}") - self.console.print(f"[bold red]❌ 更新文件条目失败: {e}[/bold red]") - return False def sync_readme(self) -> bool: """ - 同步README.md和design.json,确保内容一致性 - 返回bool表示是否成功 + 同步README.md和design.json,利用哈希值判断一致性并更新。 + 如果哈希不一致,更新design.json中的readme_path和readme_hash以匹配当前README文件。 + 返回bool表示是否成功。 """ - logger.info("开始同步README.md和design.json") - # 读取README.md + logger.info("开始同步README.md和design.json,使用哈希值判断") readme_path = self.output_dir / "README.md" + design_path = self.output_dir / "design.json" + + # 检查文件是否存在 if not readme_path.exists(): logger.error(f"README.md不存在于 {self.output_dir}") - self.console.print( - f"[bold red]❌ README.md不存在于 {self.output_dir}[/bold red]" - ) + self.console.print(f"[bold red]❌ README.md不存在于 {self.output_dir}[/bold red]") return False - try: - with open(readme_path, "r", encoding="utf-8") as f: - readme_content = f.read() - except Exception as e: - logger.error(f"读取README.md失败: {e}") - self.console.print(f"[bold red]❌ 读取README.md失败: {e}[/bold red]") - return False - - # 加载design.json - design_path = self.output_dir / "design.json" if not design_path.exists(): logger.error(f"design.json不存在于 {self.output_dir}") - self.console.print( - f"[bold red]❌ design.json不存在于 {self.output_dir}[/bold red]" - ) + self.console.print(f"[bold red]❌ design.json不存在于 {self.output_dir}[/bold red]") return False + try: - with open(design_path, "r", encoding="utf-8") as f: - design_data = json.load(f) - design = DesignModel(**design_data) - except Exception as e: - logger.error(f"加载design.json失败: {e}") - self.console.print(f"[bold red]❌ 加载design.json失败: {e}[/bold red]") - return False - - # 调用LLM比较和同步 - system_prompt = ( - "你是一个软件架构师。比较README.md内容和design.json,识别不一致之处,并建议更新。" - "返回严格的JSON对象,包含以下字段:\n" - "- needs_update: bool, 是否需要更新\n" - "- update_type: 'readme' 或 'design' 或 'both', 指示哪个需要更新\n" - "- updates: 对象,描述具体的更新内容\n" - "注意:仅返回JSON,不要其他文本。" - ) - user_prompt = f"README.md内容:\n{readme_content}\n\ndesign.json内容:\n{json.dumps(design.model_dump(), indent=2)}" - - try: - result = self._call_llm(system_prompt, user_prompt, temperature=0.2) - needs_update = result.get("needs_update", False) - if not needs_update: - logger.info("README.md和design.json已同步,无需更新") - self.console.print( - "[green]✅ README.md和design.json已同步,无需更新[/green]" - ) + # 创建 DesignManager 实例 + design_manager = DesignManager(design_path) + # 加载设计模型 + design = design_manager.load_design() + # 校验哈希 + if design_manager.validate_readme_hash(design, readme_path): + logger.info("README.md和design.json哈希一致,无需更新") + self.console.print("[green]✅ README.md和design.json哈希一致,无需更新[/green]") return True - - update_type = result.get("update_type", "") - updates = result.get("updates", {}) - if update_type == "readme": - # 更新README.md - new_readme = updates.get("new_readme", readme_content) - with open(readme_path, "w", encoding="utf-8") as f: - f.write(new_readme) - logger.info("已更新README.md") - self.console.print("[green]✅ README.md已更新[/green]") - elif update_type == "design": - # 更新design.json - new_design_data = updates.get("new_design", design.model_dump()) - design = DesignModel(**new_design_data) - with open(design_path, "w", encoding="utf-8") as f: - json.dump(new_design_data, f, indent=2, ensure_ascii=False) - logger.info("已更新design.json") - self.console.print("[green]✅ design.json已更新[/green]") - elif update_type == "both": - # 更新两者 - new_readme = updates.get("new_readme", readme_content) - new_design_data = updates.get("new_design", design.model_dump()) - with open(readme_path, "w", encoding="utf-8") as f: - f.write(new_readme) - design = DesignModel(**new_design_data) - with open(design_path, "w", encoding="utf-8") as f: - json.dump(new_design_data, f, indent=2, ensure_ascii=False) - logger.info("已同步更新README.md和design.json") - self.console.print("[green]✅ README.md和design.json已同步更新[/green]") else: - logger.warning(f"未知的update_type: {update_type}") - self.console.print( - f"[yellow]⚠ 未知的update_type: {update_type}[/yellow]" - ) - return False - return True + logger.info("README.md和design.json哈希不一致,开始同步") + # 同步设计模型以匹配当前README + design = design_manager.sync_with_readme(design, readme_path) + # 保存更新后的设计模型 + design_manager.save_design(design) + logger.info("已更新design.json中的readme_path和readme_hash") + self.console.print("[green]✅ 已同步design.json,更新了readme_path和readme_hash[/green]") + return True except Exception as e: - logger.error(f"同步README.md失败: {e}") - self.console.print(f"[bold red]❌ 同步README.md失败: {e}[/bold red]") + logger.error(f"同步失败: {e}") + self.console.print(f"[bold red]❌ 同步失败: {e}[/bold red]") return False diff --git a/src/llm_codegen/design_generator.py b/src/llm_codegen/design_generator.py index d2bf739..c7c3027 100644 --- a/src/llm_codegen/design_generator.py +++ b/src/llm_codegen/design_generator.py @@ -1,12 +1,194 @@ import json from pathlib import Path +import pathspec +import os +import concurrent.futures +import traceback + from typing import Optional, Dict, Any, List import sys from loguru import logger from rich.console import Console from .core import CodeGenerator -from .models import DesignModel, FileModel, FileStatus, LLMResponse +from .models import DesignModel, FileModel +from .design_manager import DesignManager + + +def get_non_ignored_files(root_path: Path): + """ + 模拟 git 行为:递归遍历目录,应用每一层的 .gitignore 规则,返回所有未被忽略的文件。 + """ + result_files = [] + + # 用于缓存每一层目录的 .gitignore 规则 (key: 目录路径, value: PathSpec 对象) + # 根目录的规则作为基础 + specs = {} + + # 预加载根目录的 .gitignore (如果存在) + root_gitignore = root_path / ".gitignore" + if root_gitignore.exists(): + with open(root_gitignore, "r", encoding="utf-8") as f: + # 'gitwildmatch' 是 Git 使用的通配符模式 + specs[root_path.resolve()] = pathspec.PathSpec.from_lines("gitwildmatch", f) + + # 使用 walk 进行遍历,这样可以按目录层级处理 + # os.walk 返回 (dirpath, dirnames, filenames) + # 我们手动将其转换为 Path 对象以便操作 + for dirpath_str, dirnames, filenames in os.walk(root_path): + current_dir = Path(dirpath_str) + current_dir_resolved = current_dir.resolve() + + # 1. 【核心逻辑】检查当前目录是否有 .gitignore + # 如果有,读取并合并到当前目录的规则中 + gitignore_file = current_dir / ".gitignore" + if ( + gitignore_file.exists() and gitignore_file != root_gitignore + ): # 避免重复加载根目录 + with open(gitignore_file, "r", encoding="utf-8") as f: + lines = f.readlines() + # 如果父目录有规则,需要继承吗? + # Git 的逻辑是:子目录的 .gitignore 仅对子目录及其后代有效。 + # 但匹配文件时,需要结合从根到当前的所有层级的规则。 + # 简单做法:为每个目录单独存一个 spec,匹配时从当前目录向上回溯查找所有适用的 spec。 + # 优化做法:为了性能,我们可以构建一个从根到当前的“累积 spec”或者在匹配时动态组合。 + # 这里采用更稳健的“动态查找”策略:匹配文件时,检查该文件相对于各个祖先目录的路径是否被该祖先的 .gitignore 命中。 + + specs[current_dir_resolved] = pathspec.PathSpec.from_lines( + "gitwildmatch", lines + ) + + # 2. 过滤文件夹 (如果文件夹被忽略,就不需要进入该文件夹了) + # 注意:.git 目录通常应该被跳过 + if ".git" in dirnames: + dirnames.remove(".git") + + # 检查 dirnames 中的文件夹是否被当前或祖先的 .gitignore 忽略 + # 这一步很重要,可以大幅减少遍历次数 + dirs_to_remove = [] + for d in dirnames: + full_dir_path = current_dir / d + if is_ignored(full_dir_path, root_path, specs): + dirs_to_remove.append(d) + + for d in dirs_to_remove: + dirnames.remove(d) + + # 3. 过滤文件 + for filename in filenames: + file_path = current_dir / filename + # 跳过 .gitignore 文件本身?通常不需要,除非它被上层规则忽略 + # 但 .git 目录内的文件已经在上面被排除了 + + if not is_ignored(file_path, root_path, specs): + # 返回相对于 root_path 的路径,符合你的 rglob 习惯 + result_files.append(file_path.relative_to(root_path)) + + return result_files + + +def is_ignored(file_path: Path, root_path: Path, specs: dict) -> bool: + """ + 判断一个文件是否被忽略。 + 逻辑:从文件所在目录开始,向上直到根目录,检查每一层的 .gitignore 规则。 + 只要有一层规则说“忽略”,且没有被下层规则“取消忽略”(!),则视为忽略。 + + 注意:pathspec 的 match 方法如果匹配到返回 True。 + Git 的优先级规则比较复杂(后定义的覆盖先定义的,子目录的规则针对子目录内容)。 + 为了简化且保持高准确度,我们收集从根到文件父目录的所有 spec, + 按顺序(从根到叶)应用,因为后面的规则可以覆盖前面的(特别是 ! 规则)。 + """ + if not specs: + return False + + resolved_file = file_path.resolve() + + # 构建从根目录到文件父目录的路径链 + # 例如:/a/b/c/file.txt -> 检查 /a, /a/b, /a/b/c 的 spec + current = resolved_file.parent + chain = [] + + # 向上回溯收集所有相关的目录 + while True: + if current.resolve() in specs: + chain.append(current.resolve()) + + if current == root_path or current == current.parent: # 到达根或文件系统根 + break + current = current.parent + + # 反转链,使其从根目录开始向下(因为子目录的规则优先级更高,且可以否定父目录的规则) + # Git 文档指出:父目录的 .gitignore 对整个树生效,但子目录的 .gitignore 可以重新定义。 + # 实际上,我们将所有相关目录的规则合并成一个大的 spec 列表,按从根到叶的顺序匹配。 + # pathspec 允许连续匹配,最后一个匹配的结果决定命运吗? + # 不完全是。在 Git 中,如果一个文件被父目录忽略,但子目录有一个 !file 规则,它会被包含。 + # 所以我们需要按顺序应用所有匹配的规则。 + + # 相对路径计算:针对每一个 spec 所在的目录,计算文件相对于该目录的路径 + for spec_dir in chain: + spec = specs[spec_dir] + try: + # 计算文件相对于该 .gitignore 所在目录的路径 + rel_path = file_path.relative_to(spec_dir) + # 转换为 unix 风格斜杠,因为 gitignore 规则使用 / + rel_path_str = rel_path.as_posix() + + if spec.match_file(rel_path_str): + # 如果匹配到了,我们需要知道它是“忽略”还是“不忽略”吗? + # pathspec 的 match_file 只要匹配模式就返回 True。 + # 但是 .gitignore 中的 '!' 开头表示不忽略。 + # pathspec 库在处理 'gitwildmatch' 时,会自动处理 '!' 逻辑吗? + # 查阅 pathspec 文档:PathSpec.match_file 返回布尔值。 + # 如果模式是 '!foo',它匹配 'foo' 时返回 True 吗? + # 实际上,pathspec 的设计是:你给它所有行(包括 !),它内部会处理逻辑。 + # 但是!如果是分多个文件加载的,我们需要小心。 + # 最佳实践:将当前目录及所有父目录的 .gitignore 内容合并到一个列表中,然后创建一个总的 PathSpec。 + pass + except ValueError: + continue + + # --- 修正策略:合并所有祖先的 .gitignore 内容 --- + # 由于跨文件的优先级逻辑(子目录覆盖父目录)在分开调用 match_file 时很难完美模拟, + # 最稳妥的方法是:收集从根到当前文件父目录的所有 .gitignore 行,按顺序合并,一次性匹配。 + + all_lines = [] + # 再次从根向下遍历(利用之前的 chain 反转) + for spec_dir in reversed( + chain + ): # chain 刚才收集时是从下到上,reversed 后是从上到下 + # 重新读取文件内容以保持顺序(或者我们在第一步缓存内容而不是编译好的对象) + # 为了效率,我们修改上面的逻辑缓存 lines 而不是 specs 对象,或者这里重新读 + gitignore_path = spec_dir / ".gitignore" + if gitignore_path.exists(): + with open(gitignore_path, "r", encoding="utf-8") as f: + all_lines.extend(f.readlines()) + + if not all_lines: + return False + + combined_spec = pathspec.PathSpec.from_lines("gitwildmatch", all_lines) + + # 最终相对于根目录的路径 + final_rel_path = file_path.relative_to(root_path).as_posix() + + return combined_spec.match_file(final_rel_path) + + +""" + 使用示例: + path = Path(".") + + # 获取排除 .gitignore 后的所有文件 + files = get_non_ignored_files(path) + + print(f"共找到 {len(files)} 个非忽略文件。") + print("前 10 个文件示例:") + for p in files[:10]: + print(p) + + # 如果你想筛选特定的后缀,例如 .py + # py_files = [p for p in files if p.suffix == '.py'] +""" class DesignGenerator(CodeGenerator): @@ -16,9 +198,8 @@ class DesignGenerator(CodeGenerator): self, api_key: Optional[str] = None, base_url: str = "https://api.deepseek.com", - model: str = "deepseek-reasoner", + model: str = "deepseek-chat", output_dir: str = "./generated", - log_file: Optional[str] = None, max_concurrency: int = 4, ): """初始化设计生成器。 @@ -36,13 +217,15 @@ class DesignGenerator(CodeGenerator): base_url=base_url, model=model, output_dir=output_dir, - log_file=log_file, max_concurrency=max_concurrency, ) self.console = Console() + self.history_designs: Optional[DesignModel] = None logger.info("DesignGenerator 初始化完成") - def process_design_command(self, issue_content: str, issue_type: str = "design") -> bool: + def process_design_command( + self, issue_content: str, issue_type: str = "design" + ) -> bool: """处理设计命令,基于工单内容实现设计文件的生成和增量更新逻辑。 包括源文件分析、LLM调用和合并/覆盖处理。 @@ -60,7 +243,9 @@ class DesignGenerator(CodeGenerator): analysis_result = self._analyze_issue(issue_content, issue_type) affected_files = analysis_result.get("affected_files", []) design_updates = analysis_result.get("design_updates", {}) - logger.debug(f"分析结果 - 受影响文件: {affected_files}, 设计更新: {design_updates}") + logger.debug( + f"分析结果 - 受影响文件: {affected_files}, 设计更新: {design_updates}" + ) # 2. LLM调用和文件生成/更新 generated_files: List[str] = [] @@ -87,9 +272,13 @@ class DesignGenerator(CodeGenerator): # 构建生成指令 if action == "create": - instruction = f"根据工单分析,创建文件 '{file_path}',描述: {description}" + instruction = ( + f"根据工单分析,创建文件 '{file_path}',描述: {description}" + ) else: # modify - instruction = f"根据工单分析,修改文件 '{file_path}',描述: {description}" + instruction = ( + f"根据工单分析,修改文件 '{file_path}',描述: {description}" + ) # 调用LLM生成代码 code, desc, commands = self.generate_file( @@ -125,6 +314,66 @@ class DesignGenerator(CodeGenerator): self.console.print(f"[bold red]❌ 处理设计命令失败: {e}[/bold red]") return False + def _analyze_single_file(self, file_path: Path, root_dir: Path) -> Dict[str, Any]: + """分析单个文件并返回设计信息字典。 + + Args: + file_path: 文件的绝对路径 + root_dir: 项目根目录,用于计算相对路径 + + Returns: + Dict[str, Any]: 包含 path, summary, dependencies 等信息的字典 + """ + try: + # 计算相对路径 + # 注意:这里需要确保 file_path 和 root_dir 是绝对路径或相对关系正确 + # 如果 file_path 已经是相对于 output_dir 的,则直接使用 + if file_path.is_absolute(): + rel_path = str(file_path.relative_to(root_dir)) + else: + rel_path = str(file_path) + + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + # 调用LLM分析单个文件 + system_prompt = ( + "你是一个软件架构师。分析给定的Python文件内容,返回设计信息。" + "请返回一个严格的JSON对象,必须包含以下字段:" + "1. 'summary': 文件的简短摘要(字符串,必填)。" + "2. 'dependencies': 依赖的项目内源码、资源文件的相对项目根目录的路径列表,如:['src/project_name/file.py'],如果无项目内依赖可为空列表" + "3. 'functions': 文件中定义的全局函数列表。每个函数必须是一个对象,包含:" + " - 'name': 函数名(字符串)。" + " - 'summary': 函数的简短摘要(字符串,必填)。" + " - 'inputs': 函数的输入参数列表(字符串列表)。" + " - 'outputs': 函数的输出参数列表(字符串列表)。" + "4. 'classes': 文件中定义的类列表。每个类必须是一个对象,包含:" + " - 'name': 类名(字符串)。" + " - 'summary': 类的简短摘要(字符串,必填)。" + " - 'methods': 类中定义的方法列表。每个方法必须是一个对象,包含:" + " - 'name': 方法名(字符串)。" + " - 'summary': 方法的简短摘要(字符串,必填)。" + " - 'inputs': 方法的输入参数列表(字符串列表)。" + " - 'outputs': 方法的输出参数列表(字符串列表)。" + "请确保JSON格式正确,所有必填字段(summary)都已填写。不要输出Markdown代码块标记,直接输出JSON字符串。" + ) + + user_prompt = f"文件路径: {rel_path}\n文件内容:\n{content}" + result = self._call_llm(system_prompt, user_prompt, temperature=0.2) + + # 构建文件模型 + return { + "path": rel_path, + "summary": result.get("summary", "自动分析生成"), + "dependencies": result.get("dependencies", []), + "functions": result.get("functions", []), + "classes": result.get("classes", []), + "design_updates": {}, + } + except Exception as e: + logger.error(f"分析文件 {file_path} 失败: {e}") + return None + def analyze_source_files(self, source_dir: Path) -> Dict[str, Any]: """分析源代码目录以提取设计信息,用于生成或刷新design.json。 @@ -139,40 +388,45 @@ class DesignGenerator(CodeGenerator): logger.error(f"源代码目录不存在: {source_dir}") raise FileNotFoundError(f"目录不存在: {source_dir}") - # 收集所有Python文件 - python_files = list(source_dir.rglob("*.py")) + # 确定根目录,用于计算相对路径 + root_dir = ( + self.output_dir + if source_dir.is_relative_to(self.output_dir) + else source_dir + ) + + # 收集所有需要处理的文件 + files = [ + p for p in get_non_ignored_files(root_dir) if p.is_relative_to(source_dir) + ] + logger.info(f"共需要处理 {len(files)} 个文件") design_info = {"files": [], "dependencies": {}} - # 简单分析:遍历文件并调用LLM提取信息(可优化为并发) - for file_path in python_files: - rel_path = str(file_path.relative_to(source_dir)) - try: - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - - # 调用LLM分析单个文件 - system_prompt = ( - "你是一个软件架构师。分析给定的Python文件内容,返回设计信息,包括摘要、依赖、函数和类。" - "返回严格的JSON对象,包含summary、dependencies、functions、classes字段。" - ) - user_prompt = f"文件路径: {rel_path}\n文件内容:\n{content}" - result = self._call_llm(system_prompt, user_prompt, temperature=0.2) - - # 构建文件模型 - file_model = { - "path": rel_path, - "summary": result.get("summary", "自动分析生成"), - "dependencies": result.get("dependencies", []), - "functions": result.get("functions", []), - "classes": result.get("classes", []), - "design_updates": {}, - } - design_info["files"].append(file_model) - design_info["dependencies"][rel_path] = file_model["dependencies"] - logger.debug(f"已分析文件: {rel_path}") - except Exception as e: - logger.error(f"分析文件 {rel_path} 失败: {e}") - # 跳过失败的文件 + # 使用线程池并发分析文件 + # max_workers 设置为 self.max_concurrency,与初始化参数保持一致 + with concurrent.futures.ThreadPoolExecutor( + max_workers=self.max_concurrency + ) as executor: + # 创建 future 到 file_path 的映射,以便在出错时记录日志 + future_to_path = { + executor.submit(self._analyze_single_file, p, root_dir): p + for p in files + } + + for future in concurrent.futures.as_completed(future_to_path): + file_path = future_to_path[future] + try: + result = future.result() + if result: + design_info["files"].append(result) + design_info["dependencies"][result["path"]] = result[ + "dependencies" + ] + logger.debug(f"已分析文件: {result['path']}") + except Exception as e: + # 这里的异常通常由 _analyze_single_file 内部捕获并返回 None, + # 但如果 _call_llm 抛出了未捕获的异常,会在这里被捕获 + logger.error(f"处理文件 {file_path} 时发生未预期的异常: {e}") logger.info(f"源代码分析完成,共分析 {len(design_info['files'])} 个文件") return design_info @@ -190,44 +444,143 @@ class DesignGenerator(CodeGenerator): try: # 分析源代码 design_info = self.analyze_source_files(source_dir) - + design_path = self.output_dir / "design.json" + if not self.history_designs: + design_manager = DesignManager(design_path) + self.history_designs = design_manager.load_design() # 构建DesignModel design = DesignModel( - project_name=self.design.project_name if self.design else "llm-codegen", - version=self.design.version if self.design else "1.0.0", - description=self.design.description if self.design else "基于大语言模型的代码生成工具", + project_name=self.history_designs.project_name if self.history_designs else "llm-codegen", + version=self.history_designs.version if self.history_designs else "0.0.1", + description=self.history_designs.description + if self.history_designs + else "基于大语言模型的代码生成工具", files=[FileModel(**file) for file in design_info["files"]], - commands=self.design.commands if self.design else [], - check_tools=self.design.check_tools if self.design else [], + commands=self.history_designs.commands if self.history_designs else [], + check_tools=self.history_designs.check_tools if self.history_designs else [], ) - + # 保存design.json - design_path = self.output_dir / "design.json" with open(design_path, "w", encoding="utf-8") as f: json.dump(design.model_dump(), f, indent=2, ensure_ascii=False) - - self.design = design + + self.history_designs = design logger.info(f"design.json 已刷新并保存至: {design_path}") self.console.print("[green]✅ design.json 已从源代码刷新[/green]") return True except Exception as e: - logger.error(f"从源代码刷新design.json失败: {e}") - self.console.print(f"[bold red]❌ 从源代码刷新design.json失败: {e}[/bold red]") + error_message = traceback.format_exc() + logger.error(f"从源代码刷新design.json失败: {e}, 堆栈跟踪:\n{error_message}") + self.console.print( + f"[bold red]❌ 从源代码刷新design.json失败: {e}[/bold red]" + ) return False - def run(self, readme_path: Optional[Path] = None, issue_content: Optional[str] = None) -> None: + def update_single_file_design(self, file_path: Path) -> bool: + """ + 更新design.json中单个文件的条目,基于文件内容。 + 复用 _analyze_single_file 逻辑以确保一致性。 + + Args: + file_path: 文件的绝对路径或相对于 output_dir 的路径 + + Returns: + bool: 是否成功更新 + """ + logger.info(f"开始更新design.json中文件条目: {file_path}") + + # 确保 design 已加载 + if not self.history_designs: + design_path = self.output_dir / "design.json" + if not design_path.exists(): + logger.error(f"design.json不存在于 {self.output_dir}") + self.console.print(f"[bold red]❌ design.json不存在于 {self.output_dir}[/bold red]") + return False + try: + with open(design_path, "r", encoding="utf-8") as f: + design_data = json.load(f) + self.history_designs = DesignModel(**design_data) + except Exception as e: + logger.error(f"加载design.json失败: {e}") + self.console.print(f"[bold red]❌ 加载design.json失败: {e}[/bold red]") + return False + + # 确定文件绝对路径 + abs_file_path = file_path if file_path.is_absolute() else (self.output_dir / file_path) + + # 复用 _analyze_single_file 获取设计信息 + # 注意:_analyze_single_file 期望 file_path 是 Path 对象 + file_info = self._analyze_single_file(abs_file_path, self.output_dir) + + if not file_info: + logger.error(f"分析文件 {file_path} 失败,无法更新design.json") + return False + + # 查找或创建文件条目 + file_model = None + for f in self.history_designs.files: + if f.path == file_info["path"]: + file_model = f + break + + if file_model is None: + # 创建新条目 + new_file = FileModel(**file_info) + self.history_designs.files.append(new_file) + logger.info(f"在design.json中创建了新文件条目: {file_info['path']}") + else: + # 更新现有条目 + # 使用 Pydantic 的 model_update 方法或直接赋值 + # 这里我们直接赋值,因为 file_info 的结构与 FileModel 匹配 + file_model.summary = file_info.get("summary", file_model.summary) + file_model.dependencies = file_info.get("dependencies", file_model.dependencies) + file_model.functions = file_info.get("functions", file_model.functions) + file_model.classes = file_info.get("classes", file_model.classes) + logger.info(f"更新了design.json中的文件条目: {file_info['path']}") + + # 保存更新后的design.json + design_path = self.output_dir / "design.json" + try: + with open(design_path, "w", encoding="utf-8") as f: + json.dump(self.history_designs.model_dump(), f, indent=2, ensure_ascii=False) + logger.info(f"design.json已更新,文件条目: {file_info['path']}") + self.console.print(f"[green]✅ design.json中文件条目 {file_info['path']} 已更新[/green]") + return True + except Exception as e: + logger.error(f"保存design.json失败: {e}") + self.console.print(f"[bold red]❌ 保存design.json失败: {e}[/bold red]") + return False + + + def run( + self, + readme_path: Optional[Path] = None, + issue_content: Optional[str] = None, + update_file: Optional[Path] = None # 新增:处理单个文件更新 + ) -> None: """主执行流程,用于集成到命令行接口。 Args: readme_path: README文件路径,可选 issue_content: 工单内容字符串,可选 + update_file: 单个文件路径,用于更新design.json中对应条目,可选 """ - if readme_path: + if update_file: + # 处理单个文件更新 + logger.info(f"开始处理单个文件更新: {update_file}") + success = self.update_single_file_design(update_file) + if success: + logger.info(f"单个文件更新完成: {update_file}") + self.console.print(f"[green]✅ 单个文件更新完成: {update_file}[/green]") + else: + logger.error(f"单个文件更新失败: {update_file}") + self.console.print(f"[bold red]❌ 单个文件更新失败: {update_file}[/bold red]") + sys.exit(1) + elif readme_path: self.readme_content = self.parse_readme(readme_path) - self.design = self.generate_design_json() + self.history_designs = self.sync_readme() logger.info("已从README生成design.json") - - if issue_content: + elif issue_content: success = self.process_design_command(issue_content) if success: logger.info("设计命令处理完成") @@ -235,10 +588,11 @@ class DesignGenerator(CodeGenerator): logger.error("设计命令处理失败") sys.exit(1) else: - logger.warning("未提供工单内容,仅生成或刷新设计文件") + logger.warning("未提供任何输入参数,仅生成或刷新设计文件") + # 可以添加默认行为,但当前为空 if __name__ == "__main__": # 示例用法 generator = DesignGenerator() - generator.run() + generator.run() \ No newline at end of file diff --git a/src/llm_codegen/design_manager.py b/src/llm_codegen/design_manager.py new file mode 100644 index 0000000..a61edc3 --- /dev/null +++ b/src/llm_codegen/design_manager.py @@ -0,0 +1,136 @@ +from pathlib import Path +from typing import Optional +import hashlib +import json +from .models import DesignModel +from .utils import read_file, write_file + + +class DesignManager: + """设计管理类,用于处理 design.json 文件的加载、保存、README 哈希计算、校验和同步功能。""" + + def __init__(self, design_file_path: Optional[Path] = None): + """初始化 DesignManager。 + + 参数: + design_file_path: design.json 文件的可选路径,如果提供,则可在后续方法中省略路径参数。 + """ + self.design_file_path = design_file_path + + def load_design(self, file_path: Optional[Path] = None) -> DesignModel: + """加载 design.json 文件并解析为 DesignModel 对象。 + + 参数: + file_path: design.json 文件路径,如果为 None,则使用初始化时设置的路径。 + + 返回: + DesignModel: 解析后的设计模型。 + + 异常: + FileNotFoundError: 如果文件不存在。 + ValueError: 如果 JSON 解析或模型验证失败。 + """ + if file_path is None: + if self.design_file_path is None: + raise ValueError("未提供 design.json 文件路径") + file_path = self.design_file_path + file_path = Path(file_path) + if not file_path.exists(): + raise FileNotFoundError(f"design.json 文件不存在: {file_path}") + + content = read_file(str(file_path)) + try: + design_dict = json.loads(content) + design = DesignModel.parse_obj(design_dict) + return design + except json.JSONDecodeError as e: + raise ValueError(f"JSON 解析失败: {e}") + except Exception as e: + raise ValueError(f"模型验证失败: {e}") + + def save_design(self, design: DesignModel, file_path: Optional[Path] = None) -> None: + """将 DesignModel 对象保存为 design.json 文件。 + + 参数: + design: 要保存的设计模型。 + file_path: 保存的文件路径,如果为 None,则使用初始化时设置的路径。 + + 异常: + ValueError: 如果未提供文件路径且初始化时未设置。 + """ + if file_path is None: + if self.design_file_path is None: + raise ValueError("未提供 design.json 文件路径") + file_path = self.design_file_path + file_path = Path(file_path) + + design_dict = design.dict(exclude_none=True) + content = json.dumps(design_dict, indent=2, ensure_ascii=False) + write_file(str(file_path), content) + + @staticmethod + def compute_readme_hash(readme_path: Path) -> str: + """计算 README 文件的 SHA256 哈希值。 + + 参数: + readme_path: README 文件路径(例如 README.md)。 + + 返回: + str: 十六进制字符串表示的 SHA256 哈希值。 + + 异常: + FileNotFoundError: 如果文件不存在。 + """ + readme_path = Path(readme_path) + if not readme_path.exists(): + raise FileNotFoundError(f"README 文件不存在: {readme_path}") + + with open(readme_path, 'rb') as f: + content = f.read() + hash_obj = hashlib.sha256(content) + return hash_obj.hexdigest() + + def validate_readme_hash(self, design: DesignModel, readme_path: Optional[Path] = None) -> bool: + """校验 README 文件的哈希值与 design.json 中存储的哈希值是否一致。 + + 参数: + design: 设计模型,包含存储的 readme_hash。 + readme_path: README 文件路径,如果为 None,则使用 design.readme_path。 + + 返回: + bool: 如果哈希一致或 readme_hash 为 None(视为无效),返回 True;否则返回 False。 + """ + if readme_path is None: + if design.readme_path is None: + return False # 没有存储的路径,无法校验 + readme_path = Path(design.readme_path) + else: + readme_path = Path(readme_path) + + if design.readme_hash is None: + return False # 没有存储的哈希,视为无效 + + try: + computed_hash = self.compute_readme_hash(readme_path) + return computed_hash == design.readme_hash + except FileNotFoundError: + return False # 文件不存在,校验失败 + + def sync_with_readme(self, design: DesignModel, readme_path: Path) -> DesignModel: + """同步 design.json 与 README 文件,更新 readme_path 和 readme_hash。 + + 参数: + design: 要更新的设计模型。 + readme_path: README 文件路径。 + + 返回: + DesignModel: 更新后的设计模型(原地修改并返回同一个对象)。 + """ + readme_path = Path(readme_path) + if not readme_path.exists(): + raise FileNotFoundError(f"README 文件不存在: {readme_path}") + + new_hash = self.compute_readme_hash(readme_path) + design.readme_path = str(readme_path) + design.readme_hash = new_hash + return design diff --git a/src/llm_codegen/diff_applier.py b/src/llm_codegen/diff_applier.py deleted file mode 100644 index c0138d0..0000000 --- a/src/llm_codegen/diff_applier.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Diff 应用模块,使用 unidiff2 解析和应用 unified diff 格式。""" -import os -from typing import List, Dict, Any -from unidiff import PatchSet, Hunk - -def _clean_path(path: str) -> str: - """清理路径,移除 a/ 或 b/ 前缀。""" - if path.startswith('a/'): - return path[2:] - if path.startswith('b/'): - return path[2:] - return path - -def parse_diff(diff: str) -> List[str]: - """ - 解析 unified diff 字符串,提取受影响的文件路径。 - - Args: - diff: unified diff 格式的字符串。 - - Returns: - 文件路径列表。 - """ - try: - patch_set = PatchSet(diff) - files = set() - for patch in patch_set: - if patch.target_file and patch.target_file != '/dev/null': - cleaned_path = _clean_path(patch.target_file) - files.add(cleaned_path) - return list(files) - except Exception: - # 解析失败时返回空列表,避免干扰 - return [] - -def _apply_single_patch_to_content(file_content_lines: List[str], patch_hunks: List[Hunk]) -> List[str]: - """ - 将一个文件的补丁(多个hunk)应用到其内容上。 - - Args: - file_content_lines: 文件内容的行列表(每行可能带换行符)。 - patch_hunks: 针对该文件的一个或多个Hunk对象列表。 - - Returns: - 应用了补丁后的新内容行列表。 - """ - # 从后往前处理 hunk,避免行号变化影响后续 hunk - sorted_hunks = sorted(patch_hunks, key=lambda x: x.source_start, reverse=True) - current_lines = file_content_lines[:] - - for hunk in sorted_hunks: - source_start = hunk.source_start - 1 # 转换为0索引 - source_len = hunk.source_length - - # 从 diff 中提取源行内容,去除所有尾随空白 - source_lines_from_diff = [line.value.rstrip() for line in hunk.source_lines()] - - # 提取实际文件对应行,并去除所有尾随空白(包括换行符) - actual_source_lines = current_lines[source_start : source_start + source_len] - actual_source_for_comparison = [line.rstrip() for line in actual_source_lines] - - if source_lines_from_diff != actual_source_for_comparison: - raise ValueError(f"Hunk at line {hunk.source_start} does not match source file content.") - - # 构建目标内容:去除尾随空白后统一添加换行符 - new_part = [line.value.rstrip() + '\n' for line in hunk.target_lines()] - - # 替换原内容区域 - current_lines = (current_lines[:source_start] + - new_part + - current_lines[source_start + source_len:]) - - return current_lines - -def apply_diff(diff: str, target_dir: str = ".") -> Dict[str, Any]: - """ - 应用 unified diff 到指定目录。 - - Args: - diff: unified diff 格式的字符串。 - target_dir: 目标目录路径,默认为当前目录。 - - Returns: - 字典,包含 success, message, applied_files, error_details。 - """ - result = { - 'success': False, - 'message': '', - 'applied_files': [], - 'error_details': '' - } - - if not diff or diff.strip() == '': - result['message'] = 'Diff string is empty' - return result - - try: - patch_set = PatchSet(diff) - affected_files = [] - for patch in patch_set: - if patch.target_file and patch.target_file != '/dev/null': - cleaned_path = _clean_path(patch.target_file) - affected_files.append(cleaned_path) - except Exception as e: - result['message'] = f"Failed to parse diff: {str(e)}" - result['error_details'] = str(e) - return result - - if not os.path.isdir(target_dir): - result['message'] = f"Target directory does not exist: {target_dir}" - return result - - try: - for patch_obj in patch_set: - target_path = _clean_path(patch_obj.target_file) - if not target_path or target_path == '/dev/null': - continue - - full_file_path = os.path.join(target_dir, target_path) - - source_path = _clean_path(patch_obj.source_file) - if source_path == '/dev/null': - original_content_lines = [] - else: - try: - with open(full_file_path, 'r', encoding='utf-8') as f: - original_content_lines = f.readlines() - except FileNotFoundError: - original_content_lines = [] - - modified_content_lines = _apply_single_patch_to_content( - original_content_lines, - list(patch_obj) - ) - - os.makedirs(os.path.dirname(full_file_path), exist_ok=True) - with open(full_file_path, 'w', encoding='utf-8', newline='') as f: - f.writelines(modified_content_lines) - - result['success'] = True - result['message'] = 'Diff applied successfully' - result['applied_files'] = affected_files - - except Exception as e: - result['message'] = f"Error while applying diff: {str(e)}" - result['error_details'] = str(e) - - return result - -# 如果作为脚本运行,可以提供简单的测试 -if __name__ == "__main__": - # 示例用法 - sample_diff = """--- a/old_file.txt -+++ b/new_file.txt -@@ -1 +1 @@ --Hello World -+Hello Universe -""" - - print("Testing apply_diff with unidiff2...") - res = apply_diff(sample_diff, ".") - print(res) diff --git a/src/llm_codegen/enhance_generator.py b/src/llm_codegen/enhance_generator.py index 971640d..33fdff4 100644 --- a/src/llm_codegen/enhance_generator.py +++ b/src/llm_codegen/enhance_generator.py @@ -1,10 +1,9 @@ -import json from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Optional from loguru import logger -from rich.console import Console from .core import CodeGenerator +from .design_manager import DesignManager # 添加导入 class EnhanceGenerator(CodeGenerator): @@ -38,7 +37,6 @@ class EnhanceGenerator(CodeGenerator): base_url=base_url, model=model, output_dir=output_dir, - log_file=log_file, max_concurrency=max_concurrency ) logger.info("EnhanceGenerator 初始化完成") @@ -57,6 +55,33 @@ class EnhanceGenerator(CodeGenerator): logger.info(f"开始处理增强工单: {issue_file_path}") self.console.print(f"[bold blue]🔧 处理增强工单: {issue_file_path}[/bold blue]") + # 添加README哈希检查逻辑 + try: + # 尝试加载design.json以检查README哈希 + design_file_path = Path("design.json") + if not design_file_path.exists(): + # 如果在当前目录找不到,尝试在输出目录中查找 + design_file_path = self.output_dir / "design.json" + + dm = DesignManager(design_file_path) + design = dm.load_design() + + # 获取README路径 + readme_path = Path(design.readme_path) if design.readme_path else Path("README.md") + + # 校验哈希 + if not dm.validate_readme_hash(design, readme_path): + warning_msg = f"README文件 {readme_path} 的哈希值与design.json中记录的不一致,可能存在内容变更。" + logger.warning(warning_msg) + self.console.print(f"[yellow]⚠ {warning_msg}[/yellow]") + else: + logger.info("README哈希校验通过") + except Exception as e: + # 如果无法检查哈希,记录错误但不停止流程 + error_msg = f"检查README哈希失败: {e}" + logger.error(error_msg) + self.console.print(f"[yellow]⚠ {error_msg}[/yellow]") + # 1. 读取工单文件内容 try: with open(issue_file_path, 'r', encoding='utf-8') as f: @@ -65,7 +90,7 @@ class EnhanceGenerator(CodeGenerator): except Exception as e: logger.error(f"读取工单文件失败: {e}") self.console.print(f"[bold red]❌ 读取工单文件失败: {e}[/bold red]") - return False + return (False, []) # 2. 调用 LLM 分析工单,获取变更计划 try: @@ -74,15 +99,14 @@ class EnhanceGenerator(CodeGenerator): logger.error(f"分析工单失败: {e}") self.console.print(f"[bold red]❌ 分析工单失败: {e}[/bold red]") raise e - return False + return (False, []) affected_files = analysis_result.get("affected_files", []) - design_updates = analysis_result.get("design_updates", {}) if not affected_files: logger.warning("工单分析未发现需要变更的文件") self.console.print("[yellow]⚠ 工单分析未发现需要变更的文件[/yellow]") - return True # 无变更,视为成功 + return (True, []) # 无变更,视为成功 logger.info(f"分析到 {len(affected_files)} 个受影响文件") self.console.print(f"[green]📋 分析到 {len(affected_files)} 个受影响文件[/green]") @@ -96,7 +120,7 @@ class EnhanceGenerator(CodeGenerator): except ValueError as e: logger.error(f"拓扑排序失败,检测到循环依赖: {e}, dependencies: {dependencies}") self.console.print(f"[bold red]❌ 拓扑排序失败,检测到循环依赖: {e}[/bold red]") - return False + return (False, []) # 根据排序顺序获取文件信息 sorted_file_infos = [] @@ -168,26 +192,7 @@ class EnhanceGenerator(CodeGenerator): # 继续处理其他文件,但记录失败 continue - # 6. 更新 design.json 以反映变更 - if generated_files: - try: - self._update_design(generated_files, design_updates) - logger.info(f"已更新 design.json,包含 {len(generated_files)} 个文件变更") - self.console.print(f"[green]✅ 已更新 design.json[/green]") - except Exception as e: - logger.error(f"更新 design.json 失败: {e}") - self.console.print(f"[bold red]❌ 更新 design.json 失败: {e}[/bold red]") - # 不返回 False,因为文件已生成,仅记录错误 - - # 7. 可选:执行全局命令或检查(如有需要,可从 design.json 读取) - # 此处可根据设计添加,例如运行检查工具 - if self.design and self.design.commands: - logger.info("开始执行项目命令") - for cmd in self.design.commands: - success = self.execute_command(cmd, cwd=self.output_dir) - if not success: - logger.warning(f"命令执行失败,但继续: {cmd}") - logger.info("增强处理流程完成") self.console.print("[bold green]🎉 增强处理流程完成[/bold green]") - return True + # 返回 True 表示成功, 返回生成的文件列表 + return (True, affected_files) diff --git a/src/llm_codegen/fix_generator.py b/src/llm_codegen/fix_generator.py index a6ede50..de0f6fd 100644 --- a/src/llm_codegen/fix_generator.py +++ b/src/llm_codegen/fix_generator.py @@ -1,8 +1,9 @@ -import json +from typing import List + from pathlib import Path -from typing import List, Optional from .core import CodeGenerator from .models import OutputFormat +from .design_manager import DesignManager class FixGenerator(CodeGenerator): @@ -16,7 +17,7 @@ class FixGenerator(CodeGenerator): self, bug_issue_path: Path, output_format: OutputFormat = OutputFormat.FULL, - ) -> bool: + ) -> (bool, List[Path]): """ 处理 fix 命令逻辑:读取 Bug 工单,分析变更,生成并应用修复代码。 @@ -34,7 +35,34 @@ class FixGenerator(CodeGenerator): except Exception as e: self.logger.error(f"读取 Bug 工单文件失败: {e}") self.console.print(f"[bold red]❌ 读取 Bug 工单文件失败: {e}[/bold red]") - return False + return (False, []) + + # 添加 README 哈希检查逻辑 + try: + design_file_path = self.output_dir / "design.json" + if design_file_path.exists(): + design_manager = DesignManager(design_file_path) + design = design_manager.load_design() + readme_path = None + if design.readme_path: + readme_path = Path(design.readme_path) + else: + readme_path = self.output_dir / "README.md" + + if readme_path and readme_path.exists(): + if not design_manager.validate_readme_hash(design, readme_path): + warning_msg = "README 哈希不一致,当前内容可能与设计不符" + self.logger.warning(warning_msg) + self.console.print(f"[bold yellow]⚠ 警告: {warning_msg}[/bold yellow]") + else: + self.logger.info("README 哈希检查通过") + else: + self.logger.warning("README 文件不存在,无法进行哈希检查") + else: + self.logger.warning("design.json 文件不存在,无法检查 README 哈希") + except Exception as e: + self.logger.warning(f"检查 README 哈希时出错: {e}") + self.console.print(f"[bold yellow]⚠ 警告: 检查 README 哈希时出错: {e}[/bold yellow]") # 调用 LLM 分析工单,获取变更计划 try: @@ -42,16 +70,15 @@ class FixGenerator(CodeGenerator): except Exception as e: self.logger.error(f"分析工单失败: {e}") self.console.print(f"[bold red]❌ 分析工单失败: {e}[/bold red]") - return False + return (False, []) affected_files = analysis.get("affected_files", []) - design_updates = analysis.get("design_updates", {}) successful_files = [] for file_info in affected_files: file_path = file_info.get("path") action = file_info.get("action") # 'create' 或 'modify' - description = file_info.get("description", "") + # description = file_info.get("description", "") dependencies = file_info.get("dependencies", []) if action == "modify": @@ -72,7 +99,7 @@ class FixGenerator(CodeGenerator): # 生成修复代码 instruction = ( - f"根据 Bug 工单修复文件 '{file_path}'。工单摘要: {issue_content[:200]}..." + f"根据 Bug 工单修复文件 '{file_path}'..." ) code, desc, commands = self.generate_file( file_path=file_path, @@ -103,7 +130,7 @@ class FixGenerator(CodeGenerator): elif action == "create": # 创建新文件:无需现有内容 instruction = ( - f"根据 Bug 工单创建文件 '{file_path}'。工单摘要: {issue_content[:200]}..." + f"根据 Bug 工单创建文件 '{file_path}'..." ) code, desc, commands = self.generate_file( file_path=file_path, @@ -129,18 +156,11 @@ class FixGenerator(CodeGenerator): for cmd in commands: self.execute_command(cmd, cwd=self.output_dir) - # 添加新条目到 design.json - self._update_design([file_path], design_updates) - - # 如果有设计更新,整体更新 design.json - if design_updates and successful_files: - self._update_design(successful_files, design_updates) - if successful_files: self.logger.info(f"修复成功,处理了 {len(successful_files)} 个文件") self.console.print(f"[green]✅ 修复成功,处理了 {len(successful_files)} 个文件[/green]") - return True + return (True, successful_files) else: self.logger.error("修复失败,没有文件被成功处理") self.console.print("[bold red]❌ 修复失败,没有文件被成功处理[/bold red]") - return False + return (False, []) diff --git a/src/llm_codegen/init_generator.py b/src/llm_codegen/init_generator.py index e4d63c0..3565df6 100644 --- a/src/llm_codegen/init_generator.py +++ b/src/llm_codegen/init_generator.py @@ -1,12 +1,13 @@ -import json from pathlib import Path from typing import Optional -from .core import BaseGenerator +from .core import CodeGenerator from loguru import logger # 确保日志可用 +from .design_generator import DesignGenerator +from .design_manager import DesignManager # 新增导入 -class InitGenerator(BaseGenerator): - """处理 init 命令的生成器类,继承自 BaseGenerator,用于从 README 初始化项目。""" +class InitGenerator(CodeGenerator): + """处理 init 命令的生成器类,继承自 CodeGenerator,用于从 README 初始化项目。""" def __init__( self, @@ -14,7 +15,6 @@ class InitGenerator(BaseGenerator): base_url: str = "https://api.deepseek.com", model: str = "deepseek-reasoner", output_dir: str = "./generated", - log_file: Optional[str] = None, max_concurrency: int = 4 ): """初始化 InitGenerator。 @@ -27,7 +27,13 @@ class InitGenerator(BaseGenerator): log_file: 日志文件路径。 max_concurrency: 最大并发数。 """ - super().__init__(api_key, base_url, model, output_dir, log_file, max_concurrency) + super().__init__(api_key, base_url, model, output_dir, max_concurrency) + self.design_generator = DesignGenerator( + api_key=api_key, + base_url=base_url, + output_dir=output_dir, + max_concurrency=max_concurrency + ) def run(self, readme_path: Path) -> None: """处理 init 命令逻辑:根据 README.md 初始化项目。 @@ -36,16 +42,23 @@ class InitGenerator(BaseGenerator): readme_path: README 文件路径。 """ logger.info(f"开始初始化项目,README路径: {readme_path}") - self.console.print(f"[bold]🚀 开始初始化项目...[/bold]") + self.console.print("[bold]🚀 开始初始化项目...[/bold]") # 1. 读取 README self.readme_content = self.parse_readme(readme_path) logger.info("README读取完成") # 2. 生成 design.json - self.design = self.generate_design_json() + self.design_generator.run(readme_path, None) logger.info("design.json生成完成") + # 计算README哈希并更新design.json + design_manager = DesignManager(Path(self.output_dir) / "design.json") + self.design = design_manager.load_design() + design_manager.sync_with_readme(self.design, readme_path) + design_manager.save_design(self.design) + logger.info("README哈希计算并存储到design.json完成") + # 3. 获取文件列表和依赖 files, dependencies = self.get_project_structure() logger.info(f"获取到 {len(files)} 个待生成文件") diff --git a/src/llm_codegen/llm_client.py b/src/llm_codegen/llm_client.py new file mode 100644 index 0000000..008ffed --- /dev/null +++ b/src/llm_codegen/llm_client.py @@ -0,0 +1,160 @@ +import json +import time +from pathlib import Path +from typing import List, Dict, Any, Optional + +from openai import OpenAI +from loguru import logger +import pendulum + + +class LLMClient: + """LLM 客户端,负责与模型交互并管理响应记录。""" + + def __init__( + self, + api_key: str, + base_url: str = "https://api.deepseek.com", + model: str = "deepseek-reasoner", + output_dir: Path = Path("./generated"), + max_retries: int = 3, + retry_delay: float = 1.0, + ): + """ + 初始化 LLM 客户端。 + + Args: + api_key: API 密钥。 + base_url: API 基础 URL。 + model: 使用的模型名称。 + output_dir: 输出目录,用于保存响应文件。 + logger: 日志记录器,若为 None 则使用 loguru 的默认 logger。 + max_retries: 最大重试次数。 + retry_delay: 重试间隔(秒),指数退避的基数。 + """ + self.api_key = api_key + self.base_url = base_url + self.model = model + self.output_dir = Path(output_dir) + self.max_retries = max_retries + self.retry_delay = retry_delay + + self.client = OpenAI(api_key=self.api_key, base_url=self.base_url) + self.responses_dir = self.output_dir / "llm_responses" + self.responses_dir.mkdir(parents=True, exist_ok=True) + + def call( + self, + messages: List[Dict[str, str]], + temperature: float = 0.2, + expect_json: bool = True, + ) -> Dict[str, Any]: + """ + 调用 LLM 并返回解析后的结果。 + + Args: + messages: OpenAI 格式的消息列表,如 [{"role": "system", "content": "..."}, ...] + temperature: 温度参数。 + expect_json: 是否期望 JSON 输出(如果为 True,将设置 response_format 并解析 JSON)。 + + Returns: + 若 expect_json 为 True,返回解析后的字典; + 否则返回 {"content": str}。 + + Raises: + ValueError: JSON 解析失败或 LLM 返回空内容。 + Exception: 重试后仍失败,则抛出最后一次异常。 + """ + logger.debug(f"调用 LLM,模型: {self.model},消息数: {len(messages)}") + + last_exception = None + for attempt in range(self.max_retries): + try: + # 构建请求参数 + kwargs = { + "model": self.model, + "messages": messages, + "temperature": temperature, + } + if expect_json: + kwargs["response_format"] = {"type": "json_object"} + + response = self.client.chat.completions.create(**kwargs) + message = response.choices[0].message + content = message.content + reasoning_content = getattr(message, "reasoning_content", None) + + # 保存响应记录 + self._save_response( + messages=messages, + content=content, + reasoning_content=reasoning_content, + temperature=temperature, + expect_json=expect_json, + ) + + # 处理输出 + if expect_json: + if not content: + raise ValueError("LLM 返回空内容") + try: + result = json.loads(content) + except json.JSONDecodeError as e: + logger.error(f"JSON 解析失败: {e}\n原始内容: {content[:500]}") + raise ValueError(f"LLM 返回的不是有效 JSON: {content[:200]}") from e + return result + else: + return {"content": content or ""} + + except Exception as e: + last_exception = e + logger.warning(f"LLM 调用失败 (尝试 {attempt + 1}/{self.max_retries}): {e}") + if attempt < self.max_retries - 1: + delay = self.retry_delay * (2 ** attempt) # 指数退避 + logger.info(f"等待 {delay:.1f} 秒后重试...") + time.sleep(delay) + else: + logger.error(f"LLM 调用最终失败: {e}") + raise last_exception + + # 理论上不会执行到这里,但保留以防万一 + raise last_exception or RuntimeError("LLM 调用失败,未捕获具体异常") + + def _save_response( + self, + messages: List[Dict[str, str]], + content: str, + reasoning_content: Optional[str], + temperature: float, + expect_json: bool, + ) -> None: + """ + 将 LLM 响应保存到文件中。 + + Args: + messages: 发送的消息列表。 + content: 返回的内容。 + reasoning_content: 思考内容(若存在)。 + temperature: 温度参数。 + expect_json: 是否期望 JSON。 + """ + timestamp = pendulum.now().format("YYYYMMDD_HHmmss_SSS") + filename = f"response_{timestamp}.json" + filepath = self.responses_dir / filename + + record = { + "timestamp": timestamp, + "model": self.model, + "messages": messages, + "content": content, + "reasoning_content": reasoning_content, + "temperature": temperature, + "expect_json": expect_json, + } + + try: + with open(filepath, "w", encoding="utf-8") as f: + json.dump(record, f, indent=2, ensure_ascii=False) + logger.debug(f"LLM 响应已保存: {filepath.name}") + except Exception as e: + logger.warning(f"保存 LLM 响应失败: {e}") \ No newline at end of file diff --git a/src/llm_codegen/models.py b/src/llm_codegen/models.py index 54d287d..d5de11e 100644 --- a/src/llm_codegen/models.py +++ b/src/llm_codegen/models.py @@ -22,15 +22,15 @@ class FunctionModel(BaseModel): """函数模型,对应 design.json 中的 functions 字段。""" name: str summary: str - inputs: List[str] - outputs: List[str] + inputs: List[str] = Field(default_factory=list) # 确保默认为空列表 + outputs: List[str] = Field(default_factory=list) # 确保默认为空列表 class ClassModel(BaseModel): """类模型,对应 design.json 中的 classes 字段。""" name: str summary: str - methods: List[str] + methods: List[FunctionModel] class FileModel(BaseModel): @@ -59,6 +59,8 @@ class DesignModel(BaseModel): files: List[FileModel] commands: List[str] = Field(default_factory=list) check_tools: List[str] = Field(default_factory=list) + readme_path: Optional[str] = Field(default=None, description="README文件的路径") + readme_hash: Optional[str] = Field(default=None, description="README文件的SHA256哈希值") # 模型用于工单 @@ -92,10 +94,11 @@ class StateModel(BaseModel): readme_path: str -# 可选:通用响应模型,用于 LLM 调用 +# 通用响应模型,用于 LLM 调用 class LLMResponse(BaseModel): """LLM 响应模型,用于解析 generate_file 方法的返回。""" code: str description: str commands: List[str] = Field(default_factory=list) output_format: OutputFormat = Field(default=OutputFormat.FULL, description="输出格式,可选值为 'full' 或 'diff',默认为 'full'") + reasoning_content: Optional[str] = Field(default=None, description="生成代码时的推理内容")