2026-03-06 16:36:40 +00:00
|
|
|
|
# core_utils.py - 跨 handler 共用的核心工具函数
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
|
|
from rich.console import Console
|
|
|
|
|
|
|
|
|
|
|
|
import config
|
|
|
|
|
|
from constants import CHG_DELETE
|
|
|
|
|
|
from database.db_manager import DBManager
|
|
|
|
|
|
from database.models import ChangeHistory, FunctionalRequirement, Project
|
|
|
|
|
|
from core.code_generator import CodeGenerator
|
|
|
|
|
|
from core.requirement_analyzer import RequirementAnalyzer
|
|
|
|
|
|
from utils.output_writer import (
|
|
|
|
|
|
patch_signatures_with_url,
|
|
|
|
|
|
write_function_signatures_json,
|
|
|
|
|
|
write_project_readme,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
console = Console()
|
2026-03-06 17:20:01 +00:00
|
|
|
|
db = DBManager()
|
2026-03-06 16:36:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# 变更历史
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def log_change(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
project_id: int,
|
|
|
|
|
|
change_type: str,
|
|
|
|
|
|
summary: str,
|
|
|
|
|
|
module: str = "",
|
|
|
|
|
|
req_id: int = None,
|
|
|
|
|
|
status: str = "pending",
|
|
|
|
|
|
) -> ChangeHistory:
|
|
|
|
|
|
"""
|
|
|
|
|
|
记录变更历史。
|
|
|
|
|
|
将 change_type / module / req_id / summary 序列化为 JSON 写入 changes 字段。
|
|
|
|
|
|
"""
|
2026-03-06 17:06:05 +00:00
|
|
|
|
changes_payload = {
|
|
|
|
|
|
"change_type": change_type,
|
|
|
|
|
|
"module": module,
|
|
|
|
|
|
"req_id": req_id,
|
|
|
|
|
|
"summary": summary,
|
|
|
|
|
|
"logged_at": datetime.utcnow().isoformat(),
|
|
|
|
|
|
}
|
|
|
|
|
|
changes_text = json.dumps(changes_payload, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
2026-03-06 17:26:49 +00:00
|
|
|
|
history = ChangeHistory(project_id=project_id,
|
|
|
|
|
|
changes=changes_text,
|
|
|
|
|
|
status=status)
|
2026-03-06 17:20:01 +00:00
|
|
|
|
# ✅ 只传模型真实存在的字段
|
2026-03-06 17:26:49 +00:00
|
|
|
|
db.create_change_history(history)
|
|
|
|
|
|
return history
|
2026-03-06 17:20:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_change_histories(project_id: int, status: str = None) -> list[dict]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取变更历史列表,并将 changes 字段反序列化为结构化 dict。
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
每条记录为 dict,包含:
|
|
|
|
|
|
id, project_id, change_time, status,
|
|
|
|
|
|
change_type, module, req_id, summary, logged_at
|
|
|
|
|
|
"""
|
|
|
|
|
|
histories = db.list_change_histories(project_id, status=status)
|
|
|
|
|
|
result = []
|
|
|
|
|
|
for h in histories:
|
|
|
|
|
|
try:
|
|
|
|
|
|
payload = json.loads(h.changes)
|
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
|
# 兼容旧格式纯文本
|
|
|
|
|
|
payload = {"summary": h.changes, "change_type": "unknown",
|
|
|
|
|
|
"module": "", "req_id": None, "logged_at": ""}
|
|
|
|
|
|
result.append({
|
|
|
|
|
|
"id": h.id,
|
|
|
|
|
|
"project_id": h.project_id,
|
|
|
|
|
|
"change_time": h.change_time,
|
|
|
|
|
|
"status": h.status,
|
|
|
|
|
|
**payload, # change_type / module / req_id / summary / logged_at
|
|
|
|
|
|
})
|
|
|
|
|
|
return result
|
2026-03-06 16:36:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# 需求过滤
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def active_reqs(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
reqs: List[FunctionalRequirement],
|
2026-03-06 16:36:40 +00:00
|
|
|
|
) -> List[FunctionalRequirement]:
|
|
|
|
|
|
"""过滤掉已软删除(status='deleted')的需求。"""
|
|
|
|
|
|
return [r for r in reqs if r.status != "deleted"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# 语言扩展名
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def get_ext(language: str) -> str:
|
|
|
|
|
|
ext_map = {
|
2026-03-06 17:20:01 +00:00
|
|
|
|
"python": ".py",
|
2026-03-06 16:36:40 +00:00
|
|
|
|
"javascript": ".js",
|
|
|
|
|
|
"typescript": ".ts",
|
2026-03-06 17:20:01 +00:00
|
|
|
|
"java": ".java",
|
|
|
|
|
|
"go": ".go",
|
|
|
|
|
|
"rust": ".rs",
|
2026-03-06 16:36:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
return ext_map.get((language or "python").lower(), ".txt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# LLM 批量生成:签名
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def generate_signatures(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
analyzer: RequirementAnalyzer,
|
|
|
|
|
|
func_reqs: List[FunctionalRequirement],
|
|
|
|
|
|
knowledge_text: str = "",
|
2026-03-06 16:36:40 +00:00
|
|
|
|
) -> List[dict]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
调用 analyzer 批量生成函数签名,
|
|
|
|
|
|
逐条打印进度(成功 / 降级)。
|
|
|
|
|
|
"""
|
2026-03-06 17:20:01 +00:00
|
|
|
|
|
2026-03-06 16:36:40 +00:00
|
|
|
|
def on_progress(i, t, req, sig, err):
|
|
|
|
|
|
status = "[red]✗ 降级[/red]" if err else "[green]✓[/green]"
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
f" {status} [{i}/{t}] REQ.{req.index_no:02d} "
|
|
|
|
|
|
f"[cyan]{req.function_name}[/cyan]"
|
|
|
|
|
|
+ (f" | [red]{err}[/red]" if err else "")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return analyzer.build_function_signatures_batch(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
func_reqs=func_reqs,
|
|
|
|
|
|
knowledge=knowledge_text,
|
|
|
|
|
|
on_progress=on_progress,
|
2026-03-06 16:36:40 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# LLM 批量生成:代码
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def generate_code(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
generator: CodeGenerator,
|
|
|
|
|
|
project: Project,
|
|
|
|
|
|
func_reqs: List[FunctionalRequirement],
|
|
|
|
|
|
output_dir: str,
|
|
|
|
|
|
knowledge_text: str = "",
|
|
|
|
|
|
signatures: List[dict] = None,
|
2026-03-06 16:36:40 +00:00
|
|
|
|
) -> list:
|
|
|
|
|
|
"""
|
|
|
|
|
|
调用 generator 批量生成代码文件,
|
|
|
|
|
|
逐条打印进度(成功 / 失败)。
|
|
|
|
|
|
"""
|
2026-03-06 17:20:01 +00:00
|
|
|
|
|
2026-03-06 16:36:40 +00:00
|
|
|
|
def on_progress(i, t, req, code_file, err):
|
|
|
|
|
|
if err:
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
f" [red]✗[/red] [{i}/{t}] {req.function_name}"
|
|
|
|
|
|
f" | [red]{err}[/red]"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
f" [green]✓[/green] [{i}/{t}] {req.function_name}"
|
|
|
|
|
|
f" → [dim]{code_file.file_path}[/dim]"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return generator.generate_batch(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
func_reqs=func_reqs,
|
|
|
|
|
|
output_dir=output_dir,
|
|
|
|
|
|
language=project.language,
|
|
|
|
|
|
knowledge=knowledge_text,
|
|
|
|
|
|
signatures=signatures,
|
|
|
|
|
|
on_progress=on_progress,
|
2026-03-06 16:36:40 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# README 写入
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def write_readme(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
project: Project,
|
|
|
|
|
|
func_reqs: List[FunctionalRequirement],
|
|
|
|
|
|
output_dir: str,
|
2026-03-06 16:36:40 +00:00
|
|
|
|
) -> str:
|
|
|
|
|
|
"""生成项目 README.md 并写入输出目录,返回文件路径。"""
|
|
|
|
|
|
req_lines = "\n".join(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
f"{i + 1}. **{r.title}** (`{r.function_name}`) — {r.description}"
|
2026-03-06 16:36:40 +00:00
|
|
|
|
for i, r in enumerate(func_reqs)
|
|
|
|
|
|
)
|
|
|
|
|
|
modules = list({r.module for r in func_reqs if r.module})
|
2026-03-06 17:20:01 +00:00
|
|
|
|
path = write_project_readme(
|
|
|
|
|
|
output_dir=output_dir,
|
|
|
|
|
|
project_name=project.name,
|
|
|
|
|
|
project_description=project.description or "",
|
|
|
|
|
|
requirements_summary=req_lines,
|
|
|
|
|
|
modules=modules,
|
2026-03-06 16:36:40 +00:00
|
|
|
|
)
|
|
|
|
|
|
console.print(f"[green]✓ README 已生成: {path}[/green]")
|
|
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# 签名 URL 回填 & 保存
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def patch_and_save_signatures(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
project: Project,
|
|
|
|
|
|
signatures: List[dict],
|
|
|
|
|
|
code_files: list,
|
|
|
|
|
|
output_dir: str,
|
|
|
|
|
|
file_name: str = "function_signatures.json",
|
2026-03-06 16:36:40 +00:00
|
|
|
|
) -> str:
|
|
|
|
|
|
"""将代码文件路径回填到签名 URL 字段,并重新写入 JSON 文件。"""
|
|
|
|
|
|
ext = get_ext(project.language)
|
|
|
|
|
|
func_name_to_url = {
|
|
|
|
|
|
cf.file_name.replace(ext, ""): cf.file_path
|
|
|
|
|
|
for cf in code_files
|
|
|
|
|
|
}
|
|
|
|
|
|
patch_signatures_with_url(signatures, func_name_to_url)
|
|
|
|
|
|
path = write_function_signatures_json(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
output_dir=output_dir,
|
|
|
|
|
|
signatures=signatures,
|
|
|
|
|
|
project_name=project.name,
|
|
|
|
|
|
project_description=project.description or "",
|
|
|
|
|
|
file_name=file_name,
|
2026-03-06 16:36:40 +00:00
|
|
|
|
)
|
|
|
|
|
|
console.print(f"[green]✓ 签名 URL 已回填: {path}[/green]")
|
|
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
# 主签名 JSON 合并
|
|
|
|
|
|
# ══════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def merge_signatures_to_main(
|
2026-03-06 17:20:01 +00:00
|
|
|
|
project: Project,
|
|
|
|
|
|
new_sigs: List[dict],
|
|
|
|
|
|
output_dir: str,
|
|
|
|
|
|
main_file: str = "function_signatures.json",
|
2026-03-06 16:36:40 +00:00
|
|
|
|
) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
将 new_sigs 合并(upsert by name)到主签名 JSON 文件中。
|
|
|
|
|
|
若主文件不存在则新建。
|
|
|
|
|
|
"""
|
|
|
|
|
|
main_path = os.path.join(output_dir, main_file)
|
|
|
|
|
|
if os.path.exists(main_path):
|
|
|
|
|
|
with open(main_path, "r", encoding="utf-8") as f:
|
|
|
|
|
|
main_doc = json.load(f)
|
|
|
|
|
|
existing_sigs = main_doc.get("functions", [])
|
|
|
|
|
|
else:
|
|
|
|
|
|
existing_sigs = []
|
|
|
|
|
|
main_doc = {
|
2026-03-06 17:20:01 +00:00
|
|
|
|
"project": project.name,
|
2026-03-06 16:36:40 +00:00
|
|
|
|
"description": project.description or "",
|
2026-03-06 17:20:01 +00:00
|
|
|
|
"functions": [],
|
2026-03-06 16:36:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sig_map = {s["name"]: s for s in existing_sigs}
|
|
|
|
|
|
for sig in new_sigs:
|
|
|
|
|
|
sig_map[sig["name"]] = sig
|
|
|
|
|
|
main_doc["functions"] = list(sig_map.values())
|
|
|
|
|
|
|
|
|
|
|
|
with open(main_path, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(main_doc, f, ensure_ascii=False, indent=2)
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
f"[green]✓ 主签名 JSON 已更新: {main_path}"
|
|
|
|
|
|
f"(共 {len(main_doc['functions'])} 条)[/green]"
|
2026-03-06 17:20:01 +00:00
|
|
|
|
)
|