项目查询与变更

This commit is contained in:
liusongtao 2026-03-05 17:24:49 +08:00
parent 6bd0bfda4d
commit 34b3f10c07
6 changed files with 557 additions and 790 deletions

View File

@ -5,8 +5,8 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
# ── LLM ────────────────────────────────────────────── # ── LLM ──────────────────────────────────────────────
LLM_API_KEY = os.getenv("OPENAI_API_KEY", "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR") LLM_API_KEY = os.getenv("OPENAI_API_KEY", "")
LLM_API_BASE = os.getenv("OPENAI_BASE_URL", "https://openapi.monica.im/v1") LLM_API_BASE = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o") LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o")
LLM_TIMEOUT = int(os.getenv("LLM_TIMEOUT", "60")) LLM_TIMEOUT = int(os.getenv("LLM_TIMEOUT", "60"))
LLM_MAX_RETRY = int(os.getenv("LLM_MAX_RETRY", "3")) LLM_MAX_RETRY = int(os.getenv("LLM_MAX_RETRY", "3"))
@ -121,3 +121,5 @@ MODULE_CLASSIFY_PROMPT_TEMPLATE = """\
只输出 JSON 数组不要有任何额外说明 只输出 JSON 数组不要有任何额外说明
""" """
CHANGE_HISTORY_FILE = "change_history.json"

View File

@ -1,10 +1,13 @@
# core/requirement_analyzer.py - 需求分解 & 函数签名生成 # core/requirement_analyzer.py - 需求分解、模块分类、函数签名生成
import json import json
from typing import List, Optional, Callable from typing import List, Optional, Callable
import config import config
from core.llm_client import LLMClient from core.llm_client import LLMClient
from database.models import FunctionalRequirement from database.models import FunctionalRequirement, ChangeHistory
from database.db_manager import DBManager
db = DBManager()
class RequirementAnalyzer: class RequirementAnalyzer:
@ -207,3 +210,36 @@ class RequirementAnalyzer:
"on_failure": {"value": "None", "description": "失败时返回 None"}, "on_failure": {"value": "None", "description": "失败时返回 None"},
}, },
} }
# ══════════════════════════════════════════════════
# 记录变更
# ══════════════════════════════════════════════════
def log_change(self, project_id: int, changes: str) -> None:
"""记录需求变更到数据库"""
change = ChangeHistory(project_id=project_id, changes=changes)
db.create_change_history(change)
def get_change_history(self, project_id: int) -> List[ChangeHistory]:
"""查询项目变更历史"""
return db.list_change_history(project_id)
def analyze_changes(self, old_reqs: List[FunctionalRequirement], new_reqs: List[FunctionalRequirement]) -> List[
str]:
"""
分析需求变更返回需要变更的代码文件列表
Args:
old_reqs: 旧的功能需求列表
new_reqs: 新的功能需求列表
Returns:
需要变更的文件列表
"""
changed_files = []
old_func_names = {req.function_name: req for req in old_reqs}
for new_req in new_reqs:
old_req = old_func_names.get(new_req.function_name)
if not old_req or old_req.description != new_req.description or old_req.module != new_req.module:
changed_files.append(new_req.function_name)
return changed_files

View File

@ -1,13 +1,12 @@
# database/db_manager.py - 数据库 CRUD 操作封装 # database/db_manager.py - 数据库 CRUD 操作封装
import os import os
import shutil from typing import List, Optional
from typing import List, Optional, Dict, Any
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session from sqlalchemy.orm import sessionmaker, Session
import config import config
from database.models import Base, Project, RawRequirement, FunctionalRequirement, CodeFile from database.models import Base, Project, RawRequirement, FunctionalRequirement, CodeFile, ChangeHistory
class DBManager: class DBManager:
@ -48,144 +47,16 @@ class DBManager:
s.commit() s.commit()
def list_projects(self) -> List[Project]: def list_projects(self) -> List[Project]:
"""返回所有项目(按创建时间倒序)"""
with self._session() as s: with self._session() as s:
return s.query(Project).order_by(Project.created_at.desc()).all() return s.query(Project).order_by(Project.created_at.desc()).all()
def delete_project(self, project_id: int, delete_output: bool = False) -> bool: def delete_project(self, project_id: int) -> None:
"""
删除指定项目及其所有关联数据级联删除
Args:
project_id: 项目 ID
delete_output: 是否同时删除磁盘上的输出目录
Returns:
True 表示删除成功False 表示项目不存在
"""
with self._session() as s: with self._session() as s:
project = s.get(Project, project_id) project = s.get(Project, project_id)
if project is None: if project:
return False
output_dir = project.output_dir
s.delete(project) s.delete(project)
s.commit() s.commit()
# 可选:删除磁盘输出目录
if delete_output and output_dir and os.path.isdir(output_dir):
shutil.rmtree(output_dir, ignore_errors=True)
return True
def get_project_stats(self, project_id: int) -> Dict[str, int]:
"""
获取项目统计信息需求数已生成代码数模块数
Returns:
{"raw_req_count": n, "func_req_count": n,
"generated_count": n, "module_count": n, "code_file_count": n}
"""
with self._session() as s:
raw_count = s.query(RawRequirement).filter_by(project_id=project_id).count()
func_reqs = (
s.query(FunctionalRequirement)
.filter_by(project_id=project_id)
.all()
)
gen_count = sum(1 for r in func_reqs if r.status == "generated")
modules = {r.module or config.DEFAULT_MODULE for r in func_reqs}
code_count = (
s.query(CodeFile)
.join(FunctionalRequirement)
.filter(FunctionalRequirement.project_id == project_id)
.count()
)
return {
"raw_req_count": raw_count,
"func_req_count": len(func_reqs),
"generated_count": gen_count,
"module_count": len(modules),
"code_file_count": code_count,
}
# ══════════════════════════════════════════════════
# Project Full Info需求-模块-代码关系)
# ══════════════════════════════════════════════════
def get_project_full_info(self, project_id: int) -> Optional[Dict[str, Any]]:
"""
获取项目完整信息包含需求-模块-代码之间的关系树
Returns::
{
"project": Project,
"stats": {...},
"modules": {
"<module_name>": {
"requirements": [
{
"req": FunctionalRequirement,
"code_files": [CodeFile, ...]
},
...
]
},
...
},
"raw_requirements": [RawRequirement, ...]
}
Returns None 若项目不存在
"""
with self._session() as s:
project = s.get(Project, project_id)
if project is None:
return None
raw_reqs = (
s.query(RawRequirement)
.filter_by(project_id=project_id)
.order_by(RawRequirement.created_at)
.all()
)
func_reqs = (
s.query(FunctionalRequirement)
.filter_by(project_id=project_id)
.order_by(FunctionalRequirement.index_no)
.all()
)
code_files = (
s.query(CodeFile)
.join(FunctionalRequirement)
.filter(FunctionalRequirement.project_id == project_id)
.all()
)
# 构建 func_req_id → [CodeFile] 映射
code_map: Dict[int, List[CodeFile]] = {}
for cf in code_files:
code_map.setdefault(cf.func_req_id, []).append(cf)
# 按模块分组
modules: Dict[str, Dict] = {}
for req in func_reqs:
mod = req.module or config.DEFAULT_MODULE
modules.setdefault(mod, {"requirements": []})
modules[mod]["requirements"].append({
"req": req,
"code_files": code_map.get(req.id, []),
})
stats = self.get_project_stats(project_id)
return {
"project": project,
"stats": stats,
"modules": modules,
"raw_requirements": raw_reqs,
}
# ══════════════════════════════════════════════════ # ══════════════════════════════════════════════════
# RawRequirement # RawRequirement
# ══════════════════════════════════════════════════ # ══════════════════════════════════════════════════
@ -237,22 +108,6 @@ class DBManager:
s.delete(obj) s.delete(obj)
s.commit() s.commit()
def bulk_update_modules(self, updates: List[dict]) -> None:
"""
批量更新功能需求的 module 字段
Args:
updates: [{"function_name": "...", "module": "..."}, ...]
"""
with self._session() as s:
name_to_module = {u["function_name"]: u["module"] for u in updates}
reqs = s.query(FunctionalRequirement).filter(
FunctionalRequirement.function_name.in_(name_to_module.keys())
).all()
for req in reqs:
req.module = name_to_module.get(req.function_name, config.DEFAULT_MODULE)
s.commit()
# ══════════════════════════════════════════════════ # ══════════════════════════════════════════════════
# CodeFile # CodeFile
# ══════════════════════════════════════════════════ # ══════════════════════════════════════════════════
@ -286,3 +141,18 @@ class DBManager:
.filter(FunctionalRequirement.project_id == project_id) .filter(FunctionalRequirement.project_id == project_id)
.all() .all()
) )
# ══════════════════════════════════════════════════
# ChangeHistory
# ══════════════════════════════════════════════════
def create_change_history(self, change: ChangeHistory) -> int:
with self._session() as s:
s.add(change)
s.commit()
s.refresh(change)
return change.id
def list_change_history(self, project_id: int) -> List[ChangeHistory]:
with self._session() as s:
return s.query(ChangeHistory).filter_by(project_id=project_id).order_by(ChangeHistory.change_time.desc()).all()

View File

@ -23,6 +23,7 @@ class Project(Base):
raw_requirements = relationship("RawRequirement", back_populates="project", cascade="all, delete-orphan") raw_requirements = relationship("RawRequirement", back_populates="project", cascade="all, delete-orphan")
functional_requirements = relationship("FunctionalRequirement", back_populates="project", cascade="all, delete-orphan") functional_requirements = relationship("FunctionalRequirement", back_populates="project", cascade="all, delete-orphan")
change_history = relationship("ChangeHistory", back_populates="project", cascade="all, delete-orphan")
def __repr__(self): def __repr__(self):
return f"<Project(id={self.id}, name={self.name!r}, language={self.language!r})>" return f"<Project(id={self.id}, name={self.name!r}, language={self.language!r})>"
@ -93,3 +94,19 @@ class CodeFile(Base):
def __repr__(self): def __repr__(self):
return f"<CodeFile(id={self.id}, file_name={self.file_name!r}, module={self.module!r})>" return f"<CodeFile(id={self.id}, file_name={self.file_name!r}, module={self.module!r})>"
class ChangeHistory(Base):
"""变更历史表"""
__tablename__ = "change_history"
id = Column(Integer, primary_key=True, autoincrement=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
change_time = Column(DateTime, default=datetime.utcnow)
changes = Column(Text, nullable=False) # 记录变更内容
status = Column(String(50), nullable=False, default="pending") # pending / confirmed
project = relationship("Project", back_populates="change_history")
def __repr__(self):
return f"<ChangeHistory(id={self.id}, project_id={self.project_id}, changes={self.changes[:20]}...)>"

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
# utils/output_writer.py - 代码文件 & JSON 输出 & 项目信息渲染 # utils/output_writer.py - 代码文件 & JSON 输出工具
import os import os
import json import json
from pathlib import Path from pathlib import Path
from typing import Dict, List, Any from typing import Dict, List
import config import config
@ -85,6 +85,15 @@ def build_signatures_document(
project_description: str, project_description: str,
signatures: List[dict], signatures: List[dict],
) -> dict: ) -> dict:
"""
构建顶层签名文档结构::
{
"project": "<name>",
"description": "<description>",
"functions": [ ... ]
}
"""
return { return {
"project": project_name, "project": project_name,
"description": project_description or "", "description": project_description or "",
@ -96,6 +105,16 @@ def patch_signatures_with_url(
signatures: List[dict], signatures: List[dict],
func_name_to_url: Dict[str, str], func_name_to_url: Dict[str, str],
) -> List[dict]: ) -> List[dict]:
"""
将代码文件路径回写到签名的 "url" 字段紧跟 "type" 之后
Args:
signatures: 签名列表in-place 修改
func_name_to_url: {函数名: 文件绝对路径}
Returns:
修改后的签名列表
"""
for sig in signatures: for sig in signatures:
url = func_name_to_url.get(sig.get("name", ""), "") url = func_name_to_url.get(sig.get("name", ""), "")
_insert_field_after(sig, after_key="type", new_key="url", new_value=url) _insert_field_after(sig, after_key="type", new_key="url", new_value=url)
@ -103,6 +122,7 @@ def patch_signatures_with_url(
def _insert_field_after(d: dict, after_key: str, new_key: str, new_value) -> None: def _insert_field_after(d: dict, after_key: str, new_key: str, new_value) -> None:
"""在有序 dict 中将 new_key 插入到 after_key 之后"""
if new_key in d: if new_key in d:
d[new_key] = new_value d[new_key] = new_value
return return
@ -120,6 +140,7 @@ def write_function_signatures_json(
project_description: str, project_description: str,
file_name: str = "function_signatures.json", file_name: str = "function_signatures.json",
) -> str: ) -> str:
"""将签名列表导出为 JSON 文件"""
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
document = build_signatures_document(project_name, project_description, signatures) document = build_signatures_document(project_name, project_description, signatures)
file_path = os.path.join(output_dir, file_name) file_path = os.path.join(output_dir, file_name)
@ -133,7 +154,9 @@ def write_function_signatures_json(
# ══════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════
def validate_signature_schema(signature: dict) -> List[str]: def validate_signature_schema(signature: dict) -> List[str]:
"""校验单个函数签名结构,返回错误列表(空列表表示通过)"""
errors: List[str] = [] errors: List[str] = []
for key in ("name", "requirement_id", "description", "type", "parameters"): for key in ("name", "requirement_id", "description", "type", "parameters"):
if key not in signature: if key not in signature:
errors.append(f"缺少顶层字段: '{key}'") errors.append(f"缺少顶层字段: '{key}'")
@ -192,192 +215,9 @@ def validate_signature_schema(signature: dict) -> List[str]:
def validate_all_signatures(signatures: List[dict]) -> Dict[str, List[str]]: def validate_all_signatures(signatures: List[dict]) -> Dict[str, List[str]]:
"""批量校验,返回 {函数名: [错误]} 字典(仅含有错误的条目)"""
return { return {
sig.get("name", f"unknown_{i}"): errs sig.get("name", f"unknown_{i}"): errs
for i, sig in enumerate(signatures) for i, sig in enumerate(signatures)
if (errs := validate_signature_schema(sig)) if (errs := validate_signature_schema(sig))
} }
# ══════════════════════════════════════════════════════
# 项目信息渲染(需求-模块-代码关系树)
# ══════════════════════════════════════════════════════
def render_project_info(full_info: Dict[str, Any], console) -> None:
"""
使用 rich 将项目完整信息需求-模块-代码关系渲染到终端
Args:
full_info: DBManager.get_project_full_info() 返回的字典
console: rich.console.Console 实例
"""
from rich.table import Table
from rich.panel import Panel
from rich.tree import Tree
from rich.text import Text
project = full_info["project"]
stats = full_info["stats"]
modules = full_info["modules"]
raw_reqs = full_info["raw_requirements"]
# ── 项目基本信息 ──────────────────────────────────
info_table = Table(show_header=False, box=None, padding=(0, 2))
info_table.add_column("key", style="bold cyan", width=14)
info_table.add_column("value", style="white")
info_table.add_row("项目 ID", str(project.id))
info_table.add_row("项目名称", project.name)
info_table.add_row("目标语言", project.language)
info_table.add_row("描述", project.description or "(无)")
info_table.add_row("输出目录", project.output_dir or "(未生成)")
info_table.add_row("创建时间", str(project.created_at)[:19])
info_table.add_row("更新时间", str(project.updated_at)[:19])
console.print(Panel(info_table, title="[bold cyan]📁 项目信息[/bold cyan]",
border_style="cyan"))
# ── 统计摘要 ──────────────────────────────────────
stat_table = Table(show_header=False, box=None, padding=(0, 3))
stat_table.add_column("k", style="bold yellow", width=16)
stat_table.add_column("v", style="white")
stat_table.add_row("原始需求数", str(stats["raw_req_count"]))
stat_table.add_row("功能需求数", str(stats["func_req_count"]))
stat_table.add_row("已生成代码", f"{stats['generated_count']} / {stats['func_req_count']}")
stat_table.add_row("功能模块数", str(stats["module_count"]))
stat_table.add_row("代码文件数", str(stats["code_file_count"]))
console.print(Panel(stat_table, title="[bold yellow]📊 统计摘要[/bold yellow]",
border_style="yellow"))
# ── 原始需求列表 ──────────────────────────────────
if raw_reqs:
raw_table = Table(title="📝 原始需求", show_lines=True)
raw_table.add_column("ID", style="dim", width=5)
raw_table.add_column("来源", style="cyan", width=8)
raw_table.add_column("文件名", width=20)
raw_table.add_column("内容摘要", width=55)
raw_table.add_column("创建时间", width=20)
for rr in raw_reqs:
raw_table.add_row(
str(rr.id),
rr.source_type,
rr.source_name or "-",
(rr.content[:80] + "...") if len(rr.content) > 80 else rr.content,
str(rr.created_at)[:19],
)
console.print(raw_table)
# ── 需求-模块-代码 关系树 ─────────────────────────
if not modules:
console.print("[dim]暂无功能需求[/dim]")
return
priority_color = {"high": "red", "medium": "yellow", "low": "green"}
status_icon = {"generated": "", "pending": "", "failed": ""}
root = Tree(
f"[bold cyan]🗂 {project.name}[/bold cyan] "
f"[dim]({stats['func_req_count']} 需求 · "
f"{stats['module_count']} 模块 · "
f"{stats['code_file_count']} 代码文件)[/dim]"
)
for module_name in sorted(modules.keys()):
mod_data = modules[module_name]
req_list = mod_data["requirements"]
mod_count = len(req_list)
gen_count = sum(1 for r in req_list if r["req"].status == "generated")
mod_branch = root.add(
f"[magenta bold]📦 {module_name}[/magenta bold] "
f"[dim]{gen_count}/{mod_count} 已生成[/dim]"
)
for item in req_list:
req = item["req"]
code_files = item["code_files"]
p_color = priority_color.get(req.priority, "white")
s_icon = status_icon.get(req.status, "")
req_label = (
f"{s_icon} [bold]{req.title}[/bold] "
f"[{p_color}][{req.priority}][/{p_color}] "
f"[dim]REQ.{req.index_no:02d} · ID={req.id}[/dim]"
)
req_branch = mod_branch.add(req_label)
# 需求详情子节点
req_branch.add(
f"[dim]函数名: [/dim][code]{req.function_name}[/code]"
)
req_branch.add(
f"[dim]描述: [/dim]{req.description[:80]}"
+ ("..." if len(req.description) > 80 else "")
)
# 代码文件子节点
if code_files:
code_branch = req_branch.add("[green]📄 代码文件[/green]")
for cf in code_files:
exists = os.path.exists(cf.file_path)
icon = "🟢" if exists else "🔴"
cf_text = Text()
cf_text.append(f"{icon} ", style="")
cf_text.append(cf.file_name, style="bold green" if exists else "bold red")
cf_text.append(f" [{cf.language}]", style="dim")
cf_text.append(f" {cf.file_path}", style="dim italic")
code_branch.add(cf_text)
else:
req_branch.add("[dim]📄 暂无代码文件[/dim]")
console.print(Panel(root, title="[bold green]🔗 需求 · 模块 · 代码 关系树[/bold green]",
border_style="green", padding=(1, 2)))
def render_project_list(projects: list, stats_map: Dict[int, Dict], console) -> None:
"""
渲染所有项目列表含统计列
Args:
projects: Project 对象列表
stats_map: {project_id: stats_dict}
console: rich.console.Console 实例
"""
from rich.table import Table
if not projects:
console.print("[dim]暂无项目,请先运行 `python main.py run` 创建项目。[/dim]")
return
table = Table(title=f"📋 项目列表(共 {len(projects)} 个)", show_lines=True)
table.add_column("ID", style="cyan bold", width=5)
table.add_column("项目名称", style="bold", width=22)
table.add_column("语言", style="magenta", width=8)
table.add_column("功能需求", style="yellow", width=8)
table.add_column("已生成", style="green", width=8)
table.add_column("模块数", width=6)
table.add_column("代码文件", width=8)
table.add_column("描述", width=28)
table.add_column("创建时间", width=20)
for p in projects:
st = stats_map.get(p.id, {})
func_total = st.get("func_req_count", 0)
gen_count = st.get("generated_count", 0)
gen_cell = (
f"[green]{gen_count}[/green]"
if gen_count == func_total and func_total > 0
else f"[yellow]{gen_count}[/yellow]"
)
table.add_row(
str(p.id),
p.name,
p.language,
str(func_total),
gen_cell,
str(st.get("module_count", 0)),
str(st.get("code_file_count", 0)),
(p.description[:28] + "...") if p.description and len(p.description) > 28
else (p.description or "[dim](无)[/dim]"),
str(p.created_at)[:19],
)
console.print(table)