项目查询与变更
This commit is contained in:
parent
6bd0bfda4d
commit
34b3f10c07
|
|
@ -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"))
|
||||||
|
|
@ -120,4 +120,6 @@ MODULE_CLASSIFY_PROMPT_TEMPLATE = """\
|
||||||
4. 若某需求确实无法归类,使用 "default" 模块
|
4. 若某需求确实无法归类,使用 "default" 模块
|
||||||
|
|
||||||
只输出 JSON 数组,不要有任何额外说明。
|
只输出 JSON 数组,不要有任何额外说明。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
CHANGE_HISTORY_FILE = "change_history.json"
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -18,11 +21,11 @@ class RequirementAnalyzer:
|
||||||
# ══════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════
|
||||||
|
|
||||||
def decompose(
|
def decompose(
|
||||||
self,
|
self,
|
||||||
raw_requirement: str,
|
raw_requirement: str,
|
||||||
project_id: int,
|
project_id: int,
|
||||||
raw_req_id: int,
|
raw_req_id: int,
|
||||||
knowledge: str = "",
|
knowledge: str = "",
|
||||||
) -> List[FunctionalRequirement]:
|
) -> List[FunctionalRequirement]:
|
||||||
"""
|
"""
|
||||||
将原始需求文本分解为功能需求列表(含模块分类)。
|
将原始需求文本分解为功能需求列表(含模块分类)。
|
||||||
|
|
@ -40,8 +43,8 @@ class RequirementAnalyzer:
|
||||||
f"【参考知识库】\n{knowledge}\n" if knowledge else ""
|
f"【参考知识库】\n{knowledge}\n" if knowledge else ""
|
||||||
)
|
)
|
||||||
prompt = config.DECOMPOSE_PROMPT_TEMPLATE.format(
|
prompt = config.DECOMPOSE_PROMPT_TEMPLATE.format(
|
||||||
raw_requirement = raw_requirement,
|
raw_requirement=raw_requirement,
|
||||||
knowledge_section = knowledge_section,
|
knowledge_section=knowledge_section,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -54,16 +57,16 @@ class RequirementAnalyzer:
|
||||||
reqs = []
|
reqs = []
|
||||||
for i, item in enumerate(items, 1):
|
for i, item in enumerate(items, 1):
|
||||||
req = FunctionalRequirement(
|
req = FunctionalRequirement(
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
raw_req_id = raw_req_id,
|
raw_req_id=raw_req_id,
|
||||||
index_no = i,
|
index_no=i,
|
||||||
title = item.get("title", f"功能{i}"),
|
title=item.get("title", f"功能{i}"),
|
||||||
description = item.get("description", ""),
|
description=item.get("description", ""),
|
||||||
function_name = item.get("function_name", f"function_{i}"),
|
function_name=item.get("function_name", f"function_{i}"),
|
||||||
priority = item.get("priority", "medium"),
|
priority=item.get("priority", "medium"),
|
||||||
module = item.get("module", config.DEFAULT_MODULE),
|
module=item.get("module", config.DEFAULT_MODULE),
|
||||||
status = "pending",
|
status="pending",
|
||||||
is_custom = False,
|
is_custom=False,
|
||||||
)
|
)
|
||||||
reqs.append(req)
|
reqs.append(req)
|
||||||
return reqs
|
return reqs
|
||||||
|
|
@ -73,9 +76,9 @@ class RequirementAnalyzer:
|
||||||
# ══════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════
|
||||||
|
|
||||||
def classify_modules(
|
def classify_modules(
|
||||||
self,
|
self,
|
||||||
func_reqs: List[FunctionalRequirement],
|
func_reqs: List[FunctionalRequirement],
|
||||||
knowledge: str = "",
|
knowledge: str = "",
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
对功能需求列表进行模块分类,返回 {function_name: module} 映射列表。
|
对功能需求列表进行模块分类,返回 {function_name: module} 映射列表。
|
||||||
|
|
@ -89,17 +92,17 @@ class RequirementAnalyzer:
|
||||||
"""
|
"""
|
||||||
req_list = [
|
req_list = [
|
||||||
{
|
{
|
||||||
"index_no": r.index_no,
|
"index_no": r.index_no,
|
||||||
"title": r.title,
|
"title": r.title,
|
||||||
"description": r.description,
|
"description": r.description,
|
||||||
"function_name": r.function_name,
|
"function_name": r.function_name,
|
||||||
}
|
}
|
||||||
for r in func_reqs
|
for r in func_reqs
|
||||||
]
|
]
|
||||||
knowledge_section = f"【参考知识库】\n{knowledge}\n" if knowledge else ""
|
knowledge_section = f"【参考知识库】\n{knowledge}\n" if knowledge else ""
|
||||||
prompt = config.MODULE_CLASSIFY_PROMPT_TEMPLATE.format(
|
prompt = config.MODULE_CLASSIFY_PROMPT_TEMPLATE.format(
|
||||||
requirements_json = json.dumps(req_list, ensure_ascii=False, indent=2),
|
requirements_json=json.dumps(req_list, ensure_ascii=False, indent=2),
|
||||||
knowledge_section = knowledge_section,
|
knowledge_section=knowledge_section,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = self.llm.chat_json(prompt)
|
result = self.llm.chat_json(prompt)
|
||||||
|
|
@ -114,10 +117,10 @@ class RequirementAnalyzer:
|
||||||
# ══════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════
|
||||||
|
|
||||||
def build_function_signature(
|
def build_function_signature(
|
||||||
self,
|
self,
|
||||||
func_req: FunctionalRequirement,
|
func_req: FunctionalRequirement,
|
||||||
requirement_id: str = "",
|
requirement_id: str = "",
|
||||||
knowledge: str = "",
|
knowledge: str = "",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
为单个功能需求生成函数签名 dict。
|
为单个功能需求生成函数签名 dict。
|
||||||
|
|
@ -135,12 +138,12 @@ class RequirementAnalyzer:
|
||||||
"""
|
"""
|
||||||
knowledge_section = f"【参考知识库】\n{knowledge}\n" if knowledge else ""
|
knowledge_section = f"【参考知识库】\n{knowledge}\n" if knowledge else ""
|
||||||
prompt = config.FUNC_SIGNATURE_PROMPT_TEMPLATE.format(
|
prompt = config.FUNC_SIGNATURE_PROMPT_TEMPLATE.format(
|
||||||
requirement_id = requirement_id or f"REQ.{func_req.index_no:02d}",
|
requirement_id=requirement_id or f"REQ.{func_req.index_no:02d}",
|
||||||
title = func_req.title,
|
title=func_req.title,
|
||||||
description = func_req.description,
|
description=func_req.description,
|
||||||
function_name = func_req.function_name,
|
function_name=func_req.function_name,
|
||||||
module = func_req.module or config.DEFAULT_MODULE,
|
module=func_req.module or config.DEFAULT_MODULE,
|
||||||
knowledge_section = knowledge_section,
|
knowledge_section=knowledge_section,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
sig = self.llm.chat_json(prompt)
|
sig = self.llm.chat_json(prompt)
|
||||||
|
|
@ -154,10 +157,10 @@ class RequirementAnalyzer:
|
||||||
raise RuntimeError(f"签名生成失败 [{func_req.function_name}]: {e}")
|
raise RuntimeError(f"签名生成失败 [{func_req.function_name}]: {e}")
|
||||||
|
|
||||||
def build_function_signatures_batch(
|
def build_function_signatures_batch(
|
||||||
self,
|
self,
|
||||||
func_reqs: List[FunctionalRequirement],
|
func_reqs: List[FunctionalRequirement],
|
||||||
knowledge: str = "",
|
knowledge: str = "",
|
||||||
on_progress: Optional[Callable] = None,
|
on_progress: Optional[Callable] = None,
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
批量生成函数签名,失败时使用降级结构。
|
批量生成函数签名,失败时使用降级结构。
|
||||||
|
|
@ -171,15 +174,15 @@ class RequirementAnalyzer:
|
||||||
与 func_reqs 等长的签名 dict 列表(索引一一对应)
|
与 func_reqs 等长的签名 dict 列表(索引一一对应)
|
||||||
"""
|
"""
|
||||||
signatures = []
|
signatures = []
|
||||||
total = len(func_reqs)
|
total = len(func_reqs)
|
||||||
|
|
||||||
for i, req in enumerate(func_reqs, 1):
|
for i, req in enumerate(func_reqs, 1):
|
||||||
req_id = f"REQ.{req.index_no:02d}"
|
req_id = f"REQ.{req.index_no:02d}"
|
||||||
try:
|
try:
|
||||||
sig = self.build_function_signature(req, req_id, knowledge)
|
sig = self.build_function_signature(req, req_id, knowledge)
|
||||||
error = None
|
error = None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sig = self._fallback_signature(req, req_id)
|
sig = self._fallback_signature(req, req_id)
|
||||||
error = e
|
error = e
|
||||||
|
|
||||||
signatures.append(sig)
|
signatures.append(sig)
|
||||||
|
|
@ -190,20 +193,53 @@ class RequirementAnalyzer:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fallback_signature(
|
def _fallback_signature(
|
||||||
req: FunctionalRequirement,
|
req: FunctionalRequirement,
|
||||||
requirement_id: str,
|
requirement_id: str,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""生成降级签名结构(LLM 失败时使用)"""
|
"""生成降级签名结构(LLM 失败时使用)"""
|
||||||
return {
|
return {
|
||||||
"name": req.function_name,
|
"name": req.function_name,
|
||||||
"requirement_id": requirement_id,
|
"requirement_id": requirement_id,
|
||||||
"description": req.description,
|
"description": req.description,
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"module": req.module or config.DEFAULT_MODULE,
|
"module": req.module or config.DEFAULT_MODULE,
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"return": {
|
"return": {
|
||||||
"type": "any",
|
"type": "any",
|
||||||
"on_success": {"value": "...", "description": "成功时返回值"},
|
"on_success": {"value": "...", "description": "成功时返回值"},
|
||||||
"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
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -16,7 +15,7 @@ class DBManager:
|
||||||
def __init__(self, db_path: str = None):
|
def __init__(self, db_path: str = None):
|
||||||
db_path = db_path or config.DB_PATH
|
db_path = db_path or config.DB_PATH
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
||||||
Base.metadata.create_all(self.engine)
|
Base.metadata.create_all(self.engine)
|
||||||
self._Session = sessionmaker(bind=self.engine)
|
self._Session = sessionmaker(bind=self.engine)
|
||||||
|
|
||||||
|
|
@ -48,143 +47,15 @@ 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
|
s.delete(project)
|
||||||
output_dir = project.output_dir
|
s.commit()
|
||||||
s.delete(project)
|
|
||||||
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
|
||||||
# ══════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════
|
||||||
|
|
@ -265,11 +120,11 @@ class DBManager:
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if existing:
|
if existing:
|
||||||
existing.file_name = code_file.file_name
|
existing.file_name = code_file.file_name
|
||||||
existing.file_path = code_file.file_path
|
existing.file_path = code_file.file_path
|
||||||
existing.module = code_file.module
|
existing.module = code_file.module
|
||||||
existing.language = code_file.language
|
existing.language = code_file.language
|
||||||
existing.content = code_file.content
|
existing.content = code_file.content
|
||||||
s.commit()
|
s.commit()
|
||||||
return existing.id
|
return existing.id
|
||||||
else:
|
else:
|
||||||
|
|
@ -285,4 +140,19 @@ class DBManager:
|
||||||
.join(FunctionalRequirement)
|
.join(FunctionalRequirement)
|
||||||
.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()
|
||||||
|
|
@ -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})>"
|
||||||
|
|
@ -92,4 +93,20 @@ class CodeFile(Base):
|
||||||
functional_requirement = relationship("FunctionalRequirement", back_populates="code_files")
|
functional_requirement = relationship("FunctionalRequirement", back_populates="code_files")
|
||||||
|
|
||||||
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
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ def write_project_readme(
|
||||||
"""生成项目 README.md"""
|
"""生成项目 README.md"""
|
||||||
module_section = ""
|
module_section = ""
|
||||||
if modules:
|
if modules:
|
||||||
module_list = "\n".join(f"- `{m}/`" for m in sorted(set(modules)))
|
module_list = "\n".join(f"- `{m}/`" for m in sorted(set(modules)))
|
||||||
module_section = f"\n## 功能模块\n\n{module_list}\n"
|
module_section = f"\n## 功能模块\n\n{module_list}\n"
|
||||||
|
|
||||||
content = f"""# {project_name}
|
content = f"""# {project_name}
|
||||||
|
|
@ -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)
|
|
||||||
Loading…
Reference in New Issue