feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -13,12 +13,7 @@ from app.schemas.agent_asset import AgentAssetRiskRuleGenerateRequest
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
from app.services.audit import AuditLogService
from app.services.risk_rule_flow_diagram import (
RiskRuleFlowDiagramField,
RiskRuleFlowDiagramRenderer,
RiskRuleFlowDiagramSpec,
build_risk_rule_flow_diagram_details,
)
from app.services.risk_rule_explainability import build_risk_rule_explainability_artifacts
from app.services.risk_rule_generation_ontology import (
BUSINESS_DOMAIN_LABELS,
DOMAIN_FIELD_PREFIXES,
@@ -38,6 +33,8 @@ from app.services.risk_rule_generation_semantics import (
build_city_consistency_draft,
build_city_consistency_params,
)
from app.services.risk_rule_generation_semantic_plan import unwrap_semantic_plan_payload
from app.services.risk_rule_dsl_validator import validate_risk_rule_draft
from app.services.risk_rule_scoring import apply_risk_score_to_draft, calculate_risk_rule_score
from app.services.runtime_chat import RuntimeChatService
@@ -54,7 +51,6 @@ class RiskRuleGenerationService:
self.rule_library_manager = rule_library_manager or AgentAssetRuleLibraryManager()
self.runtime_chat_service = runtime_chat_service or RuntimeChatService(db)
self.audit_service = AuditLogService(db)
self.flow_diagram_renderer = RiskRuleFlowDiagramRenderer()
def generate_rule_asset(
self,
@@ -98,12 +94,14 @@ class RiskRuleGenerationService:
risk_level="medium",
fields=fields,
)
draft = validate_risk_rule_draft(draft, fields=fields, natural_language=natural_language)
draft = self._align_draft_fields(
draft,
natural_language=natural_language,
risk_level="medium",
fields=fields,
)
draft = validate_risk_rule_draft(draft, fields=fields, natural_language=natural_language)
risk_score = calculate_risk_rule_score(
natural_language=natural_language,
draft=draft,
@@ -261,6 +259,7 @@ class RiskRuleGenerationService:
return None
if not isinstance(payload, dict):
return None
payload = unwrap_semantic_plan_payload(payload)
return self._sanitize_model_draft(payload, fields=fields)
def _sanitize_model_draft(
@@ -341,6 +340,8 @@ class RiskRuleGenerationService:
scoring_evidence = payload.get("risk_scoring_evidence")
if isinstance(scoring_evidence, dict):
draft["risk_scoring_evidence"] = scoring_evidence
if isinstance(payload.get("model_semantic_plan"), dict):
draft["model_semantic_plan"] = payload["model_semantic_plan"]
for key in ("formula", "message_template"):
value = self._clean_text(payload.get(key))
if value:
@@ -435,6 +436,8 @@ class RiskRuleGenerationService:
semantic_type = str(draft.get("semantic_type") or "").strip()
if semantic_type:
params["semantic_type"] = semantic_type
if isinstance(draft.get("dsl_validation"), dict):
params["dsl_validation"] = draft["dsl_validation"]
if template_key == COMPOSITE_RULE_TEMPLATE_KEY and isinstance(draft.get("rule_ir"), dict):
params["rule_ir"] = draft["rule_ir"]
for key in ("conditions", "hit_logic", "field_groups", "formula", "message_template"):
@@ -516,60 +519,28 @@ class RiskRuleGenerationService:
"business_explanation": self._clean_text(draft.get("description")),
"condition_summary": condition_summary,
"rule_ir": draft.get("rule_ir") if isinstance(draft.get("rule_ir"), dict) else {},
"model_semantic_plan": draft.get("model_semantic_plan") if isinstance(draft.get("model_semantic_plan"), dict) else {},
"flow": draft.get("flow") if isinstance(draft.get("flow"), dict) else {},
},
}
payload["flow_diagram_svg"] = self._build_flow_diagram_svg(
explainability = build_risk_rule_explainability_artifacts(
payload,
fields=[field_by_key[key] for key in field_keys if key in field_by_key],
domain=domain,
domain_label=risk_category,
risk_level=risk_level,
risk_level_label=risk_level_label,
)
payload.update(explainability)
payload["metadata"].update(
{
"semantic_plan": explainability["semantic_plan"],
"flow_model": explainability["flow_model"],
"flow_explanation": explainability["flow_explanation"],
"flow_diagram_svg": explainability["flow_diagram_svg"],
}
)
return payload
def _build_flow_diagram_svg(
self,
payload: dict[str, Any],
*,
fields: list[RiskRuleField],
domain: str,
domain_label: str | None = None,
risk_level: str,
) -> str:
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
flow = metadata.get("flow") if isinstance(metadata.get("flow"), dict) else {}
condition_summary = self._clean_text(metadata.get("condition_summary"))
diagram_fields = [
RiskRuleFlowDiagramField(key=field.key, label=field.label) for field in fields
]
details = build_risk_rule_flow_diagram_details(payload, diagram_fields)
return self.flow_diagram_renderer.render(
RiskRuleFlowDiagramSpec(
title=self._clean_text(payload.get("name")) or "风险规则判断流程",
domain_label=domain_label or BUSINESS_DOMAIN_LABELS.get(domain, "业务"),
severity=risk_level,
severity_label=RISK_LEVEL_LABELS.get(risk_level, "中风险"),
fields=tuple(diagram_fields),
start=self._clean_text(flow.get("start")) or "业务单据提交",
evidence=self._clean_text(flow.get("evidence")) or "读取规则字段",
decision=self._clean_text(flow.get("decision"))
or condition_summary
or "判断是否命中风险",
basis=(
condition_summary
or self._clean_text(flow.get("decision"))
or "根据规则字段判断"
),
pass_text=self._clean_text(flow.get("pass")) or "未命中风险,继续流转",
fail_text=self._clean_text(flow.get("fail"))
or f"命中{RISK_LEVEL_LABELS.get(risk_level, '风险')},进入人工复核",
fact_lines=details["fact_lines"],
condition_lines=details["condition_lines"],
hit_logic=str(details["hit_logic"] or ""),
)
)
@staticmethod
def _normalize_expense_category(value: str | None, domain: str) -> str | None:
if domain != AgentAssetDomain.EXPENSE.value:
@@ -759,6 +730,8 @@ class RiskRuleGenerationService:
@staticmethod
def _infer_template_key(text: str) -> str:
if any(keyword in text for keyword in ("超过", "超出", "超预算", "预算", "阈值", "早于", "晚于", "范围")):
return COMPOSITE_RULE_TEMPLATE_KEY
if any(
keyword in text
for keyword in ("一致", "匹配", "相同", "不一致", "不符", "对应", "出现在")