完善skills;测试用例生成页面功能初步实现
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -10,6 +11,45 @@ from app.db.session import SessionLocal
|
||||
from app.models.tooling import SRSExtraction, SRSRequirement, ToolJob
|
||||
from app.tools.srs_reqs_qwen import get_srs_tool
|
||||
|
||||
TYPE_TO_CHINESE = {
|
||||
"functional": "功能需求",
|
||||
"interface": "接口需求",
|
||||
"performance": "性能需求",
|
||||
"security": "安全需求",
|
||||
"reliability": "可靠性需求",
|
||||
"other": "其他需求",
|
||||
}
|
||||
|
||||
|
||||
def _build_internal_title(description: Any, fallback: str, index: int = 0) -> str:
|
||||
text = str(description or "").strip()
|
||||
if not text:
|
||||
return fallback or f"需求项 {index + 1}"
|
||||
|
||||
for separator in ("。", ";", "\n", ";", "."):
|
||||
if separator in text:
|
||||
text = text.split(separator, 1)[0].strip()
|
||||
break
|
||||
|
||||
text = text[:20].strip()
|
||||
return text or fallback or f"需求项 {index + 1}"
|
||||
|
||||
|
||||
def _normalize_requirement_type(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
if text in {"functional", "interface", "performance", "security", "reliability", "other"}:
|
||||
return text
|
||||
|
||||
chinese_map = {
|
||||
"接口需求": "interface",
|
||||
"性能需求": "performance",
|
||||
"安全需求": "security",
|
||||
"可靠性需求": "reliability",
|
||||
"其他需求": "other",
|
||||
"功能需求": "functional",
|
||||
}
|
||||
return chinese_map.get(text, "functional")
|
||||
|
||||
|
||||
def run_srs_job(job_id: int) -> None:
|
||||
db = SessionLocal()
|
||||
@@ -41,14 +81,19 @@ def run_srs_job(job_id: int) -> None:
|
||||
requirement = SRSRequirement(
|
||||
extraction_id=extraction.id,
|
||||
requirement_uid=item["id"],
|
||||
title=item.get("title") or item["id"],
|
||||
title=_build_internal_title(item.get("description"), item["id"]),
|
||||
description=item.get("description") or "",
|
||||
priority=item.get("priority") or "中",
|
||||
acceptance_criteria=item.get("acceptance_criteria") or ["待补充验收标准"],
|
||||
source_field=item.get("source_field") or "文档解析",
|
||||
section_uid=item.get("section_uid"),
|
||||
section_number=item.get("section_number"),
|
||||
section_title=item.get("section_title"),
|
||||
requirement_type=item.get("requirement_type"),
|
||||
interface_name=item.get("interface_name"),
|
||||
interface_type=item.get("interface_type"),
|
||||
data_source=item.get("data_source"),
|
||||
data_destination=item.get("data_destination"),
|
||||
sort_order=int(item.get("sort_order") or 0),
|
||||
)
|
||||
db.add(requirement)
|
||||
@@ -97,22 +142,8 @@ def ensure_upload_path(job_id: int, file_name: str) -> Path:
|
||||
|
||||
|
||||
def build_result_response(job: ToolJob, extraction: SRSExtraction) -> Dict[str, Any]:
|
||||
requirements: List[Dict[str, Any]] = []
|
||||
for item in extraction.requirements:
|
||||
requirements.append(
|
||||
{
|
||||
"id": item.requirement_uid,
|
||||
"title": item.title,
|
||||
"description": item.description,
|
||||
"priority": item.priority,
|
||||
"acceptanceCriteria": item.acceptance_criteria or [],
|
||||
"sourceField": item.source_field,
|
||||
"sectionNumber": item.section_number,
|
||||
"sectionTitle": item.section_title,
|
||||
"requirementType": item.requirement_type,
|
||||
"sortOrder": item.sort_order,
|
||||
}
|
||||
)
|
||||
requirements = [_requirement_model_to_payload(item) for item in extraction.requirements]
|
||||
raw_output = _merge_updates_into_raw_output(extraction.raw_output, requirements, extraction.document_name)
|
||||
|
||||
return {
|
||||
"jobId": job.id,
|
||||
@@ -120,10 +151,12 @@ def build_result_response(job: ToolJob, extraction: SRSExtraction) -> Dict[str,
|
||||
"generatedAt": extraction.generated_at.isoformat(),
|
||||
"statistics": extraction.statistics or {},
|
||||
"requirements": requirements,
|
||||
"rawOutput": raw_output,
|
||||
}
|
||||
|
||||
|
||||
def replace_requirements(db: Session, extraction: SRSExtraction, updates: List[Dict[str, Any]]) -> None:
|
||||
normalized_updates = [_normalize_update_payload(item, index) for index, item in enumerate(updates)]
|
||||
existing = {
|
||||
req.requirement_uid: req
|
||||
for req in db.query(SRSRequirement)
|
||||
@@ -132,51 +165,95 @@ def replace_requirements(db: Session, extraction: SRSExtraction, updates: List[D
|
||||
}
|
||||
seen_ids = set()
|
||||
|
||||
for index, item in enumerate(updates):
|
||||
for index, item in enumerate(normalized_updates):
|
||||
uid = item["id"]
|
||||
seen_ids.add(uid)
|
||||
req = existing.get(uid)
|
||||
if req is None:
|
||||
description = item.get("description") if item.get("description") is not None else ""
|
||||
req = SRSRequirement(
|
||||
extraction_id=extraction.id,
|
||||
requirement_uid=uid,
|
||||
title=item.get("title") or uid,
|
||||
description=item.get("description") if item.get("description") is not None else "",
|
||||
title=_build_internal_title(description, uid, int(item.get("sortOrder") or index)),
|
||||
description=description,
|
||||
priority=item.get("priority") or "中",
|
||||
acceptance_criteria=item.get("acceptanceCriteria") or ["待补充验收标准"],
|
||||
source_field=item.get("sourceField") or "文档解析",
|
||||
section_uid=item.get("sectionUid"),
|
||||
section_number=item.get("sectionNumber"),
|
||||
section_title=item.get("sectionTitle"),
|
||||
requirement_type=item.get("requirementType"),
|
||||
sort_order=int(item.get("sortOrder") or index),
|
||||
interface_name=item.get("interfaceName"),
|
||||
interface_type=item.get("interfaceType"),
|
||||
data_source=item.get("dataSource"),
|
||||
data_destination=item.get("dataDestination"),
|
||||
sort_order=int(item.get("sortOrder") or 0),
|
||||
)
|
||||
db.add(req)
|
||||
continue
|
||||
|
||||
req.title = item.get("title", req.title)
|
||||
req.description = item.get("description", req.description)
|
||||
req.title = _build_internal_title(req.description, req.requirement_uid, int(item.get("sortOrder") or index))
|
||||
req.priority = item.get("priority", req.priority)
|
||||
req.acceptance_criteria = item.get("acceptanceCriteria", req.acceptance_criteria)
|
||||
req.source_field = item.get("sourceField", req.source_field)
|
||||
req.section_uid = item.get("sectionUid", req.section_uid)
|
||||
req.section_number = item.get("sectionNumber", req.section_number)
|
||||
req.section_title = item.get("sectionTitle", req.section_title)
|
||||
req.requirement_type = item.get("requirementType", req.requirement_type)
|
||||
req.sort_order = int(item.get("sortOrder", index))
|
||||
req.interface_name = item.get("interfaceName", req.interface_name)
|
||||
req.interface_type = item.get("interfaceType", req.interface_type)
|
||||
req.data_source = item.get("dataSource", req.data_source)
|
||||
req.data_destination = item.get("dataDestination", req.data_destination)
|
||||
req.sort_order = int(item.get("sortOrder", req.sort_order))
|
||||
|
||||
for uid, req in existing.items():
|
||||
if uid not in seen_ids:
|
||||
db.delete(req)
|
||||
|
||||
extraction.total_requirements = len(updates)
|
||||
extraction.total_requirements = len(normalized_updates)
|
||||
extraction.statistics = {
|
||||
"total": len(updates),
|
||||
"by_type": _count_requirement_types(updates),
|
||||
}
|
||||
extraction.raw_output = {
|
||||
"document_name": extraction.document_name,
|
||||
"generated_at": extraction.generated_at.isoformat(),
|
||||
"requirements": updates,
|
||||
"total": len(normalized_updates),
|
||||
"by_type": _count_requirement_types(normalized_updates),
|
||||
}
|
||||
extraction.raw_output = _merge_updates_into_raw_output(
|
||||
extraction.raw_output,
|
||||
normalized_updates,
|
||||
extraction.document_name,
|
||||
)
|
||||
|
||||
|
||||
def list_srs_history(db: Session, user_id: int) -> List[Dict[str, Any]]:
|
||||
records: List[Tuple[ToolJob, SRSExtraction]] = (
|
||||
db.query(ToolJob, SRSExtraction)
|
||||
.join(SRSExtraction, SRSExtraction.job_id == ToolJob.id)
|
||||
.filter(ToolJob.user_id == user_id)
|
||||
.order_by(ToolJob.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
items: List[Dict[str, Any]] = []
|
||||
for job, extraction in records:
|
||||
items.append(
|
||||
{
|
||||
"jobId": job.id,
|
||||
"documentName": extraction.document_name,
|
||||
"generatedAt": extraction.generated_at.isoformat(),
|
||||
"totalRequirements": extraction.total_requirements,
|
||||
"status": job.status,
|
||||
"createdAt": job.created_at.isoformat(),
|
||||
"updatedAt": job.updated_at.isoformat(),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def delete_srs_job(db: Session, job: ToolJob) -> None:
|
||||
db.delete(job)
|
||||
db.commit()
|
||||
|
||||
|
||||
def build_srs_upload_path(job_id: int) -> Path:
|
||||
return Path("uploads") / "srs_jobs" / str(job_id)
|
||||
|
||||
|
||||
def _count_requirement_types(items: List[Dict[str, Any]]) -> Dict[str, int]:
|
||||
@@ -185,3 +262,210 @@ def _count_requirement_types(items: List[Dict[str, Any]]) -> Dict[str, int]:
|
||||
req_type = item.get("requirementType") or "functional"
|
||||
stats[req_type] = stats.get(req_type, 0) + 1
|
||||
return stats
|
||||
|
||||
|
||||
def _requirement_model_to_payload(item: SRSRequirement) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": item.requirement_uid,
|
||||
"description": item.description,
|
||||
"priority": item.priority,
|
||||
"acceptanceCriteria": item.acceptance_criteria or [],
|
||||
"sourceField": item.source_field,
|
||||
"sectionUid": item.section_uid,
|
||||
"sectionNumber": item.section_number,
|
||||
"sectionTitle": item.section_title,
|
||||
"requirementType": item.requirement_type,
|
||||
"interfaceName": item.interface_name,
|
||||
"interfaceType": item.interface_type,
|
||||
"dataSource": item.data_source,
|
||||
"dataDestination": item.data_destination,
|
||||
"sortOrder": item.sort_order,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_update_payload(item: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||||
requirement_type = _normalize_requirement_type(item.get("requirementType"))
|
||||
normalized = {
|
||||
"id": str(item.get("id") or f"REQ-{index + 1:03d}"),
|
||||
"description": str(item.get("description") or "").strip(),
|
||||
"priority": item.get("priority") or "中",
|
||||
"acceptanceCriteria": item.get("acceptanceCriteria") or ["待补充验收标准"],
|
||||
"sourceField": item.get("sourceField") or "文档解析",
|
||||
"sectionUid": item.get("sectionUid"),
|
||||
"sectionNumber": item.get("sectionNumber"),
|
||||
"sectionTitle": item.get("sectionTitle"),
|
||||
"requirementType": requirement_type,
|
||||
"interfaceName": item.get("interfaceName") or "",
|
||||
"interfaceType": item.get("interfaceType") or "",
|
||||
"dataSource": item.get("dataSource") or "",
|
||||
"dataDestination": item.get("dataDestination") or "",
|
||||
"sortOrder": int(item.get("sortOrder") or index),
|
||||
}
|
||||
|
||||
if requirement_type != "interface":
|
||||
normalized["interfaceName"] = ""
|
||||
normalized["interfaceType"] = ""
|
||||
normalized["dataSource"] = ""
|
||||
normalized["dataDestination"] = ""
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def _merge_updates_into_raw_output(
|
||||
raw_output: Dict[str, Any] | None,
|
||||
updates: List[Dict[str, Any]],
|
||||
document_name: str,
|
||||
) -> Dict[str, Any]:
|
||||
if not isinstance(raw_output, dict) or "需求内容" not in raw_output:
|
||||
return _build_raw_output_from_flat(updates, document_name)
|
||||
|
||||
result = deepcopy(raw_output)
|
||||
content = result.get("需求内容")
|
||||
if not isinstance(content, dict):
|
||||
return _build_raw_output_from_flat(updates, document_name)
|
||||
|
||||
updates_by_section = _group_updates_by_section(updates)
|
||||
_rewrite_content_requirements(content, updates_by_section)
|
||||
_append_unmatched_sections(content, updates_by_section)
|
||||
_refresh_metadata(result, updates)
|
||||
return result
|
||||
|
||||
|
||||
def _group_updates_by_section(updates: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
grouped: Dict[str, List[Dict[str, Any]]] = {}
|
||||
for item in updates:
|
||||
key = _section_key(item.get("sectionUid"), item.get("sectionNumber"), item.get("sectionTitle"))
|
||||
grouped.setdefault(key, []).append(item)
|
||||
|
||||
for values in grouped.values():
|
||||
values.sort(key=lambda value: int(value.get("sortOrder") or 0))
|
||||
return grouped
|
||||
|
||||
|
||||
def _rewrite_content_requirements(content: Dict[str, Any], updates_by_section: Dict[str, List[Dict[str, Any]]]) -> None:
|
||||
for section in content.values():
|
||||
if not isinstance(section, dict):
|
||||
continue
|
||||
|
||||
section_info = section.get("章节信息") or {}
|
||||
section_key = _section_key(
|
||||
section_info.get("章节UID"),
|
||||
section_info.get("章节编号"),
|
||||
section_info.get("章节标题"),
|
||||
)
|
||||
if section_key in updates_by_section:
|
||||
section["需求列表"] = [_to_raw_requirement_item(item) for item in updates_by_section.pop(section_key)]
|
||||
elif "需求列表" in section:
|
||||
section["需求列表"] = []
|
||||
|
||||
children = section.get("子章节")
|
||||
if isinstance(children, dict):
|
||||
_rewrite_content_requirements(children, updates_by_section)
|
||||
|
||||
|
||||
def _append_unmatched_sections(content: Dict[str, Any], updates_by_section: Dict[str, List[Dict[str, Any]]]) -> None:
|
||||
if not updates_by_section:
|
||||
return
|
||||
|
||||
orphan_key = "未归类章节"
|
||||
orphan_section = content.get(orphan_key)
|
||||
if not isinstance(orphan_section, dict):
|
||||
orphan_section = {
|
||||
"章节信息": {
|
||||
"章节编号": "",
|
||||
"章节标题": orphan_key,
|
||||
"章节级别": 1,
|
||||
},
|
||||
"需求列表": [],
|
||||
}
|
||||
content[orphan_key] = orphan_section
|
||||
|
||||
all_reqs: List[Dict[str, Any]] = orphan_section.get("需求列表") or []
|
||||
for values in updates_by_section.values():
|
||||
for item in values:
|
||||
all_reqs.append(_to_raw_requirement_item(item))
|
||||
orphan_section["需求列表"] = all_reqs
|
||||
updates_by_section.clear()
|
||||
|
||||
|
||||
def _refresh_metadata(raw_output: Dict[str, Any], updates: List[Dict[str, Any]]) -> None:
|
||||
metadata = raw_output.get("文档元数据")
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
raw_output["文档元数据"] = metadata
|
||||
|
||||
metadata["总需求数"] = len(updates)
|
||||
metadata["生成时间"] = datetime.now().isoformat()
|
||||
|
||||
type_stats: Dict[str, int] = {}
|
||||
for item in updates:
|
||||
req_type = item.get("requirementType") or "functional"
|
||||
cn_type = TYPE_TO_CHINESE.get(req_type, "其他需求")
|
||||
type_stats[cn_type] = type_stats.get(cn_type, 0) + 1
|
||||
metadata["需求类型统计"] = type_stats
|
||||
|
||||
|
||||
def _to_raw_requirement_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
req_type = item.get("requirementType") or "functional"
|
||||
raw_item = {
|
||||
"需求类型": TYPE_TO_CHINESE.get(req_type, "其他需求"),
|
||||
"需求编号": item.get("id") or "",
|
||||
"需求描述": item.get("description") or "",
|
||||
"优先级": item.get("priority") or "中",
|
||||
}
|
||||
if req_type == "interface":
|
||||
raw_item["接口名称"] = item.get("interfaceName") or ""
|
||||
raw_item["接口类型"] = item.get("interfaceType") or ""
|
||||
raw_item["数据来源"] = item.get("dataSource") or ""
|
||||
raw_item["数据目的地"] = item.get("dataDestination") or ""
|
||||
return raw_item
|
||||
|
||||
|
||||
def _build_raw_output_from_flat(updates: List[Dict[str, Any]], document_name: str) -> Dict[str, Any]:
|
||||
grouped = _group_updates_by_section(updates)
|
||||
content: Dict[str, Any] = {}
|
||||
for key, values in grouped.items():
|
||||
number, title = _parse_section_key(key)
|
||||
section_title = title or "未归类章节"
|
||||
display_key = f"{number} {section_title}".strip()
|
||||
content[display_key] = {
|
||||
"章节信息": {
|
||||
"章节编号": number,
|
||||
"章节标题": section_title,
|
||||
"章节级别": max(len(number.split(".")), 1) if number else 1,
|
||||
},
|
||||
"需求列表": [_to_raw_requirement_item(item) for item in values],
|
||||
}
|
||||
|
||||
raw_output = {
|
||||
"文档元数据": {
|
||||
"标题": document_name,
|
||||
"生成时间": datetime.now().isoformat(),
|
||||
"总需求数": len(updates),
|
||||
"需求类型统计": {},
|
||||
},
|
||||
"需求内容": content,
|
||||
}
|
||||
_refresh_metadata(raw_output, updates)
|
||||
return raw_output
|
||||
|
||||
|
||||
def _section_key(section_uid: Any, section_number: Any, section_title: Any) -> str:
|
||||
uid = str(section_uid or "").strip()
|
||||
if uid:
|
||||
return f"uid::{uid}"
|
||||
number = str(section_number or "").strip()
|
||||
title = str(section_title or "").strip()
|
||||
return f"number::{number}::title::{title}"
|
||||
|
||||
|
||||
def _parse_section_key(value: str) -> Tuple[str, str]:
|
||||
if value.startswith("uid::"):
|
||||
return "", "未归类章节"
|
||||
number = ""
|
||||
title = ""
|
||||
parts = value.split("::")
|
||||
if len(parts) >= 4:
|
||||
number = parts[1]
|
||||
title = parts[3]
|
||||
return number, title
|
||||
|
||||
Reference in New Issue
Block a user