fix: remove sha256 duplicate check so re-upload always creates new version in group
This commit is contained in:
@@ -244,3 +244,12 @@ class GovdocController(BaseController):
|
|||||||
"""获取当前生效规则集摘要。"""
|
"""获取当前生效规则集摘要。"""
|
||||||
result = await self.GovdocService.ListRules()
|
result = await self.GovdocService.ListRules()
|
||||||
return Result.success(data=result)
|
return Result.success(data=result)
|
||||||
|
|
||||||
|
@self.router.get("/rules/{ruleId}")
|
||||||
|
async def GetRuleDetail(
|
||||||
|
ruleId: str,
|
||||||
|
payload: dict[str, Any] = Depends(verify_access_token),
|
||||||
|
):
|
||||||
|
"""获取单条规则完整详情。"""
|
||||||
|
result = await self.GovdocService.GetRuleDetail(ruleId=ruleId)
|
||||||
|
return Result.success(data=result)
|
||||||
|
|||||||
@@ -21,16 +21,33 @@ class ResultAdapter:
|
|||||||
- 前端 VO 字典
|
- 前端 VO 字典
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def AdaptRunSummary(self, EngineResult: AuditResult) -> dict[str, Any]:
|
def AdaptRunSummary(
|
||||||
"""从 AuditResult.summary 提取 run 汇总字段。"""
|
self,
|
||||||
|
EngineResult: AuditResult,
|
||||||
|
Structure: list[dict[str, Any]] | None = None,
|
||||||
|
Outline: list[dict[str, Any]] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""从 AuditResult.summary 提取 run 汇总字段。
|
||||||
|
|
||||||
|
同时接受已适配的 structure / outline 列表,一并序列化进
|
||||||
|
resultSummaryJson,供前端 structure-panel / outline-panel 读取。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
s = EngineResult.summary
|
s = EngineResult.summary
|
||||||
|
aux: dict[str, Any] = {}
|
||||||
|
if Structure is not None:
|
||||||
|
aux["structure"] = Structure
|
||||||
|
if Outline is not None:
|
||||||
|
aux["outline"] = Outline
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"totalScore": s.score,
|
"totalScore": s.score,
|
||||||
"passedCount": s.passed_count,
|
"passedCount": s.passed_count,
|
||||||
"failedCount": s.failed_count,
|
"failedCount": s.failed_count,
|
||||||
"skippedCount": s.skipped_count,
|
"skippedCount": s.skipped_count,
|
||||||
"resultStatus": "pass" if s.failed_count == 0 else "fail" if s.passed_count == 0 else "partial",
|
"resultStatus": "pass" if s.failed_count == 0 else "fail" if s.passed_count == 0 else "partial",
|
||||||
"resultSummaryJson": None, # 可为后续扩展预留
|
"resultSummaryJson": json.dumps(aux, ensure_ascii=False) if aux else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def AdaptRuleResults(self, EngineResult: AuditResult) -> list[dict[str, Any]]:
|
def AdaptRuleResults(self, EngineResult: AuditResult) -> list[dict[str, Any]]:
|
||||||
|
|||||||
@@ -74,13 +74,20 @@ class GovdocRunner:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 4. 适配引擎结果
|
# 4. 适配引擎结果
|
||||||
runSummary = self.ResultAdapter.AdaptRunSummary(engineResult)
|
|
||||||
ruleResults = self.ResultAdapter.AdaptRuleResults(engineResult)
|
|
||||||
entities = self.ResultAdapter.AdaptEntities(engineResult)
|
|
||||||
structure = self.ResultAdapter.AdaptStructure(engineResult)
|
structure = self.ResultAdapter.AdaptStructure(engineResult)
|
||||||
outline = self.ResultAdapter.AdaptOutline(engineResult)
|
outline = self.ResultAdapter.AdaptOutline(engineResult)
|
||||||
|
runSummary = self.ResultAdapter.AdaptRunSummary(
|
||||||
|
engineResult,
|
||||||
|
Structure=structure,
|
||||||
|
Outline=outline,
|
||||||
|
)
|
||||||
|
ruleResults = self.ResultAdapter.AdaptRuleResults(engineResult)
|
||||||
|
entities = self.ResultAdapter.AdaptEntities(engineResult)
|
||||||
artifacts = self.ResultAdapter.AdaptArtifacts(engineResult, RunId)
|
artifacts = self.ResultAdapter.AdaptArtifacts(engineResult, RunId)
|
||||||
|
|
||||||
|
# 将 rules_path 附带到 runSummary 中,供 GetRuleDetail 后续解析
|
||||||
|
runSummary["rulesPath"] = RulesPath
|
||||||
|
|
||||||
# 5. 持久化结果
|
# 5. 持久化结果
|
||||||
await self.Storage.UpdateRunResult(RunId, runSummary)
|
await self.Storage.UpdateRunResult(RunId, runSummary)
|
||||||
await self.Storage.SaveRuleResults(RunId, ruleResults)
|
await self.Storage.SaveRuleResults(RunId, ruleResults)
|
||||||
|
|||||||
@@ -73,21 +73,18 @@ class StorageAdapter:
|
|||||||
log.info(f"[Govdoc] Run status updated: runId={RunId}, status={Status}")
|
log.info(f"[Govdoc] Run status updated: runId={RunId}, status={Status}")
|
||||||
|
|
||||||
async def UpdateRunResult(self, RunId: int, Summary: dict[str, Any]) -> None:
|
async def UpdateRunResult(self, RunId: int, Summary: dict[str, Any]) -> None:
|
||||||
"""写入 run 结果汇总字段。"""
|
"""写入 run 结果汇总字段(含 rules_path / structure / outline)。"""
|
||||||
async with GetAsyncSession() as session:
|
rules_path = Summary.get("rulesPath")
|
||||||
await session.execute(
|
set_clauses = [
|
||||||
text(
|
"total_score = :total_score",
|
||||||
"""UPDATE govdoc_runs SET
|
"passed_count = :passed_count",
|
||||||
total_score = :total_score,
|
"failed_count = :failed_count",
|
||||||
passed_count = :passed_count,
|
"skipped_count = :skipped_count",
|
||||||
failed_count = :failed_count,
|
"result_status = :result_status",
|
||||||
skipped_count = :skipped_count,
|
"result_summary_json = :result_summary_json",
|
||||||
result_status = :result_status,
|
"updated_at = now()",
|
||||||
result_summary_json = :result_summary_json,
|
]
|
||||||
updated_at = now()
|
params: dict[str, Any] = {
|
||||||
WHERE id = :rid"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"rid": RunId,
|
"rid": RunId,
|
||||||
"total_score": Summary.get("totalScore"),
|
"total_score": Summary.get("totalScore"),
|
||||||
"passed_count": Summary.get("passedCount", 0),
|
"passed_count": Summary.get("passedCount", 0),
|
||||||
@@ -95,7 +92,15 @@ class StorageAdapter:
|
|||||||
"skipped_count": Summary.get("skippedCount", 0),
|
"skipped_count": Summary.get("skippedCount", 0),
|
||||||
"result_status": Summary.get("resultStatus"),
|
"result_status": Summary.get("resultStatus"),
|
||||||
"result_summary_json": Summary.get("resultSummaryJson"),
|
"result_summary_json": Summary.get("resultSummaryJson"),
|
||||||
},
|
}
|
||||||
|
if rules_path:
|
||||||
|
set_clauses.append("rules_path = :rules_path")
|
||||||
|
params["rules_path"] = rules_path
|
||||||
|
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await session.execute(
|
||||||
|
text(f"UPDATE govdoc_runs SET {', '.join(set_clauses)} WHERE id = :rid"),
|
||||||
|
params,
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
log.info(f"[Govdoc] Run result saved: runId={RunId}")
|
log.info(f"[Govdoc] Run result saved: runId={RunId}")
|
||||||
|
|||||||
@@ -126,6 +126,11 @@ class IGovdocService(ABC):
|
|||||||
# ── 规则 ──────────────────────────────────────────────
|
# ── 规则 ──────────────────────────────────────────────
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def ListRules(self) -> dict[str, Any]:
|
async def ListRules(self, rulesPath: str | None = None) -> dict[str, Any]:
|
||||||
"""获取当前生效规则集摘要。"""
|
"""获取当前生效规则集摘要。"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def GetRuleDetail(self, ruleId: str, rulesPath: str | None = None) -> dict[str, Any]:
|
||||||
|
"""获取单条规则完整详情(名称、严重度、分类、分组、stages、消息等)。"""
|
||||||
|
...
|
||||||
|
|||||||
@@ -96,8 +96,103 @@ class GovdocServiceImpl(IGovdocService):
|
|||||||
# ── 结果与报告 ────────────────────────────────────────
|
# ── 结果与报告 ────────────────────────────────────────
|
||||||
|
|
||||||
async def GetRunResult(self, runId: int) -> dict[str, Any]:
|
async def GetRunResult(self, runId: int) -> dict[str, Any]:
|
||||||
logger.info("[Govdoc] GetRunResult placeholder — runId=%s", runId)
|
"""从 govdoc_runs + govdoc_rule_results 读取审查结果,含 structure/outline。"""
|
||||||
return {"runId": runId, "summary": {}}
|
import json as _json
|
||||||
|
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
run_row = await session.execute(
|
||||||
|
text(
|
||||||
|
"""SELECT id, document_id, status, phase, total_score, passed_count,
|
||||||
|
failed_count, skipped_count, result_status, result_summary_json,
|
||||||
|
rules_path, started_at, finished_at
|
||||||
|
FROM govdoc_runs WHERE id = :rid"""
|
||||||
|
),
|
||||||
|
{"rid": runId},
|
||||||
|
)
|
||||||
|
run_data = run_row.mappings().first()
|
||||||
|
if not run_data:
|
||||||
|
return {"runId": runId, "summary": {}, "checkedRules": [], "findings": [],
|
||||||
|
"structure": [], "outline": [], "entities": {}}
|
||||||
|
|
||||||
|
rules_rows = await session.execute(
|
||||||
|
text(
|
||||||
|
"""SELECT rule_id, rule_name, severity, category, result, skip_reason,
|
||||||
|
message, suggestion, actual, expected, evidence,
|
||||||
|
paragraph_index, paragraph_text, location_path, score
|
||||||
|
FROM govdoc_rule_results WHERE run_id = :rid"""
|
||||||
|
),
|
||||||
|
{"rid": runId},
|
||||||
|
)
|
||||||
|
rule_results = [dict(r._mapping) for r in rules_rows.fetchall()]
|
||||||
|
|
||||||
|
aux_raw = run_data.get("result_summary_json")
|
||||||
|
aux = {}
|
||||||
|
if aux_raw:
|
||||||
|
try:
|
||||||
|
aux = _json.loads(aux_raw) if isinstance(aux_raw, str) else aux_raw
|
||||||
|
except (TypeError, _json.JSONDecodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
findings = []
|
||||||
|
for rr in rule_results:
|
||||||
|
loc = {}
|
||||||
|
if rr.get("paragraph_index") is not None:
|
||||||
|
loc["paragraph_index"] = rr["paragraph_index"]
|
||||||
|
if rr.get("paragraph_text"):
|
||||||
|
loc["context"] = rr["paragraph_text"]
|
||||||
|
if rr.get("location_path"):
|
||||||
|
loc["role"] = rr["location_path"]
|
||||||
|
findings.append({
|
||||||
|
"finding_id": f"{rr['rule_id']}-{rr['paragraph_index'] or 0}",
|
||||||
|
"rule_id": rr["rule_id"],
|
||||||
|
"rule_name": rr["rule_name"],
|
||||||
|
"severity": rr["severity"],
|
||||||
|
"category": rr["category"],
|
||||||
|
"location": loc if loc else None,
|
||||||
|
"actual": rr.get("actual") or {},
|
||||||
|
"expected": rr.get("expected") or {},
|
||||||
|
"message": rr.get("message") or "",
|
||||||
|
"suggestion": rr.get("suggestion") or "",
|
||||||
|
"evidence": rr.get("evidence") or "",
|
||||||
|
"confidence": 1.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
checked_rules = []
|
||||||
|
seen = set()
|
||||||
|
for rr in rule_results:
|
||||||
|
rid = rr["rule_id"]
|
||||||
|
if rid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(rid)
|
||||||
|
status = rr.get("result", "pass")
|
||||||
|
checked_rules.append({
|
||||||
|
"rule_id": rid,
|
||||||
|
"name": rr["rule_name"],
|
||||||
|
"severity": rr["severity"],
|
||||||
|
"category": rr["category"],
|
||||||
|
"status": status if status in ("pass", "fail", "skipped") else "pass",
|
||||||
|
"skip_reason": rr.get("skip_reason"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"runId": runId,
|
||||||
|
"summary": {
|
||||||
|
"score": run_data.get("total_score", 100),
|
||||||
|
"total_findings": len(findings),
|
||||||
|
"by_severity": {},
|
||||||
|
"by_category": {},
|
||||||
|
"passed_count": run_data.get("passed_count", 0),
|
||||||
|
"failed_count": run_data.get("failed_count", 0),
|
||||||
|
"skipped_count": run_data.get("skipped_count", 0),
|
||||||
|
},
|
||||||
|
"checkedRules": checked_rules,
|
||||||
|
"findings": findings,
|
||||||
|
"structure": aux.get("structure", []),
|
||||||
|
"outline": aux.get("outline", []),
|
||||||
|
"entities": {},
|
||||||
|
}
|
||||||
|
|
||||||
async def GetRunFindings(self, runId: int) -> dict[str, Any]:
|
async def GetRunFindings(self, runId: int) -> dict[str, Any]:
|
||||||
logger.info("[Govdoc] GetRunFindings placeholder — runId=%s", runId)
|
logger.info("[Govdoc] GetRunFindings placeholder — runId=%s", runId)
|
||||||
@@ -133,6 +228,81 @@ class GovdocServiceImpl(IGovdocService):
|
|||||||
|
|
||||||
# ── 规则 ──────────────────────────────────────────────
|
# ── 规则 ──────────────────────────────────────────────
|
||||||
|
|
||||||
async def ListRules(self) -> dict[str, Any]:
|
async def ListRules(self, rulesPath: str | None = None) -> dict[str, Any]:
|
||||||
logger.info("[Govdoc] ListRules placeholder")
|
"""从 govdoc 规则 YAML 文件加载规则清单。"""
|
||||||
return {"rules": []}
|
rules = await self._load_rules_list(rulesPath)
|
||||||
|
return {"rules": rules, "total_rules": len(rules)}
|
||||||
|
|
||||||
|
async def GetRuleDetail(self, ruleId: str, rulesPath: str | None = None) -> dict[str, Any]:
|
||||||
|
"""获取单条规则完整详情(名称、严重度、stages、消息等)。"""
|
||||||
|
ruleset = await self._load_ruleset(rulesPath)
|
||||||
|
if ruleset is None:
|
||||||
|
return {"rule_id": ruleId, "name": ruleId, "severity": "info", "category": "", "group": ""}
|
||||||
|
for rule in ruleset.all_rules():
|
||||||
|
if rule.rule_id == ruleId:
|
||||||
|
return {
|
||||||
|
"rule_id": rule.rule_id,
|
||||||
|
"name": rule.name,
|
||||||
|
"severity": rule.severity,
|
||||||
|
"category": rule.category,
|
||||||
|
"group": "",
|
||||||
|
"applies_to": rule.applies_to.model_dump() if rule.applies_to else None,
|
||||||
|
"target": rule.target,
|
||||||
|
"on_missing": rule.on_missing,
|
||||||
|
"stages": [s.model_dump(exclude_none=True) for s in (rule.stages or [])],
|
||||||
|
"messages": rule.messages.model_dump() if rule.messages else {},
|
||||||
|
}
|
||||||
|
return {"rule_id": ruleId, "name": ruleId, "severity": "info", "category": "", "group": ""}
|
||||||
|
|
||||||
|
# ── 规则加载助手 ────────────────────────────────────
|
||||||
|
|
||||||
|
async def _resolve_rules_path(self, rulesPath: str | None = None) -> str | None:
|
||||||
|
"""解析规则 YAML 文件路径。
|
||||||
|
|
||||||
|
优先级:传入参数 > govdoc_runs 表记录 > None
|
||||||
|
"""
|
||||||
|
if rulesPath:
|
||||||
|
return rulesPath
|
||||||
|
# 尝试从最近的 completed run 中获取 rules_path
|
||||||
|
try:
|
||||||
|
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||||
|
from sqlalchemy import text
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
row = await session.execute(
|
||||||
|
text(
|
||||||
|
"""SELECT rules_path FROM govdoc_runs
|
||||||
|
WHERE rules_path IS NOT NULL AND status = 'completed'
|
||||||
|
ORDER BY id DESC LIMIT 1"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = row.mappings().first()
|
||||||
|
if result and result.get("rules_path"):
|
||||||
|
return result["rules_path"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _load_ruleset(self, rulesPath: str | None = None):
|
||||||
|
"""加载 rules.yaml 为 RuleSet 对象。"""
|
||||||
|
resolved = await self._resolve_rules_path(rulesPath)
|
||||||
|
if not resolved:
|
||||||
|
logger.warning("[Govdoc] Cannot resolve rules path for GetRuleDetail/ListRules")
|
||||||
|
return None
|
||||||
|
from fastapi_modules.fastapi_leaudit.govdoc_engine.dsl.loader import load_rules
|
||||||
|
return load_rules(resolved)
|
||||||
|
|
||||||
|
async def _load_rules_list(self, rulesPath: str | None = None) -> list[dict[str, Any]]:
|
||||||
|
"""加载规则列表(简要信息)。"""
|
||||||
|
ruleset = await self._load_ruleset(rulesPath)
|
||||||
|
if ruleset is None:
|
||||||
|
return []
|
||||||
|
result = []
|
||||||
|
for rule in ruleset.all_rules():
|
||||||
|
result.append({
|
||||||
|
"rule_id": rule.rule_id,
|
||||||
|
"name": rule.name,
|
||||||
|
"severity": rule.severity,
|
||||||
|
"category": rule.category,
|
||||||
|
"group": "",
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user