项目查询与变更

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()
# ── LLM ──────────────────────────────────────────────
LLM_API_KEY = os.getenv("OPENAI_API_KEY", "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR")
LLM_API_BASE = os.getenv("OPENAI_BASE_URL", "https://openapi.monica.im/v1")
LLM_API_KEY = os.getenv("OPENAI_API_KEY", "")
LLM_API_BASE = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o")
LLM_TIMEOUT = int(os.getenv("LLM_TIMEOUT", "60"))
LLM_MAX_RETRY = int(os.getenv("LLM_MAX_RETRY", "3"))
@ -120,4 +120,6 @@ MODULE_CLASSIFY_PROMPT_TEMPLATE = """\
4. 若某需求确实无法归类使用 "default" 模块
只输出 JSON 数组不要有任何额外说明
"""
"""
CHANGE_HISTORY_FILE = "change_history.json"

View File

@ -1,10 +1,13 @@
# core/requirement_analyzer.py - 需求分解 & 函数签名生成
# core/requirement_analyzer.py - 需求分解、模块分类、函数签名生成
import json
from typing import List, Optional, Callable
import config
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:
@ -18,11 +21,11 @@ class RequirementAnalyzer:
# ══════════════════════════════════════════════════
def decompose(
self,
raw_requirement: str,
project_id: int,
raw_req_id: int,
knowledge: str = "",
self,
raw_requirement: str,
project_id: int,
raw_req_id: int,
knowledge: str = "",
) -> List[FunctionalRequirement]:
"""
将原始需求文本分解为功能需求列表含模块分类
@ -40,8 +43,8 @@ class RequirementAnalyzer:
f"【参考知识库】\n{knowledge}\n" if knowledge else ""
)
prompt = config.DECOMPOSE_PROMPT_TEMPLATE.format(
raw_requirement = raw_requirement,
knowledge_section = knowledge_section,
raw_requirement=raw_requirement,
knowledge_section=knowledge_section,
)
try:
@ -54,16 +57,16 @@ class RequirementAnalyzer:
reqs = []
for i, item in enumerate(items, 1):
req = FunctionalRequirement(
project_id = project_id,
raw_req_id = raw_req_id,
index_no = i,
title = item.get("title", f"功能{i}"),
description = item.get("description", ""),
function_name = item.get("function_name", f"function_{i}"),
priority = item.get("priority", "medium"),
module = item.get("module", config.DEFAULT_MODULE),
status = "pending",
is_custom = False,
project_id=project_id,
raw_req_id=raw_req_id,
index_no=i,
title=item.get("title", f"功能{i}"),
description=item.get("description", ""),
function_name=item.get("function_name", f"function_{i}"),
priority=item.get("priority", "medium"),
module=item.get("module", config.DEFAULT_MODULE),
status="pending",
is_custom=False,
)
reqs.append(req)
return reqs
@ -73,9 +76,9 @@ class RequirementAnalyzer:
# ══════════════════════════════════════════════════
def classify_modules(
self,
func_reqs: List[FunctionalRequirement],
knowledge: str = "",
self,
func_reqs: List[FunctionalRequirement],
knowledge: str = "",
) -> List[dict]:
"""
对功能需求列表进行模块分类返回 {function_name: module} 映射列表
@ -89,17 +92,17 @@ class RequirementAnalyzer:
"""
req_list = [
{
"index_no": r.index_no,
"title": r.title,
"description": r.description,
"index_no": r.index_no,
"title": r.title,
"description": r.description,
"function_name": r.function_name,
}
for r in func_reqs
]
knowledge_section = f"【参考知识库】\n{knowledge}\n" if knowledge else ""
prompt = config.MODULE_CLASSIFY_PROMPT_TEMPLATE.format(
requirements_json = json.dumps(req_list, ensure_ascii=False, indent=2),
knowledge_section = knowledge_section,
requirements_json=json.dumps(req_list, ensure_ascii=False, indent=2),
knowledge_section=knowledge_section,
)
try:
result = self.llm.chat_json(prompt)
@ -114,10 +117,10 @@ class RequirementAnalyzer:
# ══════════════════════════════════════════════════
def build_function_signature(
self,
func_req: FunctionalRequirement,
requirement_id: str = "",
knowledge: str = "",
self,
func_req: FunctionalRequirement,
requirement_id: str = "",
knowledge: str = "",
) -> dict:
"""
为单个功能需求生成函数签名 dict
@ -135,12 +138,12 @@ class RequirementAnalyzer:
"""
knowledge_section = f"【参考知识库】\n{knowledge}\n" if knowledge else ""
prompt = config.FUNC_SIGNATURE_PROMPT_TEMPLATE.format(
requirement_id = requirement_id or f"REQ.{func_req.index_no:02d}",
title = func_req.title,
description = func_req.description,
function_name = func_req.function_name,
module = func_req.module or config.DEFAULT_MODULE,
knowledge_section = knowledge_section,
requirement_id=requirement_id or f"REQ.{func_req.index_no:02d}",
title=func_req.title,
description=func_req.description,
function_name=func_req.function_name,
module=func_req.module or config.DEFAULT_MODULE,
knowledge_section=knowledge_section,
)
try:
sig = self.llm.chat_json(prompt)
@ -154,10 +157,10 @@ class RequirementAnalyzer:
raise RuntimeError(f"签名生成失败 [{func_req.function_name}]: {e}")
def build_function_signatures_batch(
self,
func_reqs: List[FunctionalRequirement],
knowledge: str = "",
on_progress: Optional[Callable] = None,
self,
func_reqs: List[FunctionalRequirement],
knowledge: str = "",
on_progress: Optional[Callable] = None,
) -> List[dict]:
"""
批量生成函数签名失败时使用降级结构
@ -171,15 +174,15 @@ class RequirementAnalyzer:
func_reqs 等长的签名 dict 列表索引一一对应
"""
signatures = []
total = len(func_reqs)
total = len(func_reqs)
for i, req in enumerate(func_reqs, 1):
req_id = f"REQ.{req.index_no:02d}"
try:
sig = self.build_function_signature(req, req_id, knowledge)
sig = self.build_function_signature(req, req_id, knowledge)
error = None
except Exception as e:
sig = self._fallback_signature(req, req_id)
sig = self._fallback_signature(req, req_id)
error = e
signatures.append(sig)
@ -190,20 +193,53 @@ class RequirementAnalyzer:
@staticmethod
def _fallback_signature(
req: FunctionalRequirement,
requirement_id: str,
req: FunctionalRequirement,
requirement_id: str,
) -> dict:
"""生成降级签名结构LLM 失败时使用)"""
return {
"name": req.function_name,
"name": req.function_name,
"requirement_id": requirement_id,
"description": req.description,
"type": "function",
"module": req.module or config.DEFAULT_MODULE,
"parameters": {},
"description": req.description,
"type": "function",
"module": req.module or config.DEFAULT_MODULE,
"parameters": {},
"return": {
"type": "any",
"type": "any",
"on_success": {"value": "...", "description": "成功时返回值"},
"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 操作封装
import os
import shutil
from typing import List, Optional, Dict, Any
from typing import List, Optional
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
import config
from database.models import Base, Project, RawRequirement, FunctionalRequirement, CodeFile
from database.models import Base, Project, RawRequirement, FunctionalRequirement, CodeFile, ChangeHistory
class DBManager:
@ -16,7 +15,7 @@ class DBManager:
def __init__(self, db_path: str = None):
db_path = db_path or config.DB_PATH
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)
self._Session = sessionmaker(bind=self.engine)
@ -48,143 +47,15 @@ class DBManager:
s.commit()
def list_projects(self) -> List[Project]:
"""返回所有项目(按创建时间倒序)"""
with self._session() as s:
return s.query(Project).order_by(Project.created_at.desc()).all()
def delete_project(self, project_id: int, delete_output: bool = False) -> bool:
"""
删除指定项目及其所有关联数据级联删除
Args:
project_id: 项目 ID
delete_output: 是否同时删除磁盘上的输出目录
Returns:
True 表示删除成功False 表示项目不存在
"""
def delete_project(self, project_id: int) -> None:
with self._session() as s:
project = s.get(Project, project_id)
if project is None:
return False
output_dir = project.output_dir
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,
}
if project:
s.delete(project)
s.commit()
# ══════════════════════════════════════════════════
# RawRequirement
@ -237,22 +108,6 @@ class DBManager:
s.delete(obj)
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
# ══════════════════════════════════════════════════
@ -265,11 +120,11 @@ class DBManager:
.first()
)
if existing:
existing.file_name = code_file.file_name
existing.file_path = code_file.file_path
existing.module = code_file.module
existing.language = code_file.language
existing.content = code_file.content
existing.file_name = code_file.file_name
existing.file_path = code_file.file_path
existing.module = code_file.module
existing.language = code_file.language
existing.content = code_file.content
s.commit()
return existing.id
else:
@ -285,4 +140,19 @@ class DBManager:
.join(FunctionalRequirement)
.filter(FunctionalRequirement.project_id == project_id)
.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")
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):
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")
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 json
from pathlib import Path
from typing import Dict, List, Any
from typing import Dict, List
import config
@ -58,7 +58,7 @@ def write_project_readme(
"""生成项目 README.md"""
module_section = ""
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"
content = f"""# {project_name}
@ -85,6 +85,15 @@ def build_signatures_document(
project_description: str,
signatures: List[dict],
) -> dict:
"""
构建顶层签名文档结构::
{
"project": "<name>",
"description": "<description>",
"functions": [ ... ]
}
"""
return {
"project": project_name,
"description": project_description or "",
@ -96,6 +105,16 @@ def patch_signatures_with_url(
signatures: List[dict],
func_name_to_url: Dict[str, str],
) -> List[dict]:
"""
将代码文件路径回写到签名的 "url" 字段紧跟 "type" 之后
Args:
signatures: 签名列表in-place 修改
func_name_to_url: {函数名: 文件绝对路径}
Returns:
修改后的签名列表
"""
for sig in signatures:
url = func_name_to_url.get(sig.get("name", ""), "")
_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:
"""在有序 dict 中将 new_key 插入到 after_key 之后"""
if new_key in d:
d[new_key] = new_value
return
@ -120,6 +140,7 @@ def write_function_signatures_json(
project_description: str,
file_name: str = "function_signatures.json",
) -> str:
"""将签名列表导出为 JSON 文件"""
os.makedirs(output_dir, exist_ok=True)
document = build_signatures_document(project_name, project_description, signatures)
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]:
"""校验单个函数签名结构,返回错误列表(空列表表示通过)"""
errors: List[str] = []
for key in ("name", "requirement_id", "description", "type", "parameters"):
if key not in signature:
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]]:
"""批量校验,返回 {函数名: [错误]} 字典(仅含有错误的条目)"""
return {
sig.get("name", f"unknown_{i}"): errs
for i, sig in enumerate(signatures)
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)
}