feat: 增强风险规则生成引擎与预算中心页面

后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-26 09:15:14 +08:00
parent d0e946cf47
commit 0e861d8fa6
150 changed files with 14953 additions and 4099 deletions

View File

@@ -1,62 +1,25 @@
from __future__ import annotations
import hashlib
import json
from datetime import UTC, date, datetime
from decimal import Decimal
from pathlib import Path
from datetime import UTC, datetime
from sqlalchemy import inspect, select, text
from sqlalchemy import select
from app.core.agent_enums import (
AgentAssetContentType,
AgentAssetDomain,
AgentAssetStatus,
AgentAssetType,
AgentName,
AgentPermissionLevel,
AgentReviewStatus,
AgentRunSource,
AgentRunStatus,
AgentToolType,
)
from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion
from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog
from app.models.audit_log import AuditLog
from app.models.financial_record import (
AccountsPayableRecord,
AccountsReceivableRecord,
ExpenseClaim,
ExpenseClaimItem,
)
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import (
AgentAssetSpreadsheetManager,
COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME,
COMPANY_TRAVEL_EXPENSE_RULE_CODE,
COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
FINANCE_RULES_LIBRARY,
RISK_RULES_LIBRARY,
)
from app.services.expense_rule_runtime import (
build_scene_submission_standard_markdown,
build_travel_risk_control_standard_markdown,
)
from app.services.agent_foundation_constants import (
ATTACHMENT_RULE_ASSET_CODE,
ATTACHMENT_RULE_RUNTIME_CONFIG,
COMPANY_COMMUNICATION_RULE_SCENARIO_JSON,
COMPANY_COMMUNICATION_RULE_VERSION,
COMPANY_TRAVEL_RULE_SCENARIO_JSON,
COMPANY_TRAVEL_RULE_VERSION,
DEMO_EXPENSE_CLAIM_SIGNATURES,
DEMO_PAYABLE_SIGNATURES,
DEMO_RECEIVABLE_SIGNATURES,
LEGACY_RULE_CODES,
PLATFORM_DESTINATION_LOCATION_RULE_FILENAME,
)
from app.core.logging import get_logger
from app.models.agent_asset import AgentAsset
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import (
RISK_RULES_LIBRARY,
)
from app.services.agent_foundation_constants import (
PLATFORM_DESTINATION_LOCATION_RULE_FILENAME,
)
logger = get_logger("app.services.agent_foundation")
@@ -67,20 +30,51 @@ class AgentFoundationRiskRuleMixin:
manifests: list[tuple[str, dict[str, object]]] = []
for file_name in sorted(manager.list_rule_library_json_files(library=RISK_RULES_LIBRARY)):
for file_name in sorted(
manager.list_rule_library_json_files(library=RISK_RULES_LIBRARY)
):
payload = manager.read_rule_library_json(library=RISK_RULES_LIBRARY, file_name=file_name)
payload = manager.read_rule_library_json(
library=RISK_RULES_LIBRARY,
file_name=file_name,
)
if payload.get("enabled") is False:
continue
if self._is_user_generated_risk_manifest(payload):
continue
manifests.append((file_name, payload))
return manifests
@staticmethod
def _is_user_generated_risk_manifest(manifest: dict[str, object]) -> bool:
rule_code = str(manifest.get("rule_code") or "").strip().lower()
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
stability = str(metadata.get("stability") or "").strip().lower()
source_ref = str(metadata.get("source_ref") or "").strip()
if stability == "generated_draft":
return True
if source_ref == "自然语言风险规则":
return True
return ".generated_" in rule_code
@staticmethod
def _resolve_platform_risk_category(manifest: dict[str, object]) -> str:
explicit = str(manifest.get("risk_category") or "").strip()
@@ -91,7 +85,9 @@ class AgentFoundationRiskRuleMixin:
rule_code = str(manifest.get("rule_code") or "").strip().lower()
applies_to = manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {}
applies_to = (
manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {}
)
domains = {str(item or "").strip().lower() for item in applies_to.get("domains") or []}
@@ -133,7 +129,9 @@ class AgentFoundationRiskRuleMixin:
return [category] if category else ["通用"]
def _platform_risk_config_json(self, file_name: str, manifest: dict[str, object]) -> dict[str, object]:
def _platform_risk_config_json(
self, file_name: str, manifest: dict[str, object]
) -> dict[str, object]:
outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {}
@@ -191,7 +189,9 @@ class AgentFoundationRiskRuleMixin:
continue
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
metadata = (
manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
)
source_ref = str(metadata.get("source_ref") or "").strip()
@@ -255,7 +255,11 @@ class AgentFoundationRiskRuleMixin:
"Platform risk rules synced from library",
extra={"manifest_count": manifest_count, "created_count": synced, "total": len(after_codes)},
extra={
"manifest_count": manifest_count,
"created_count": synced,
"total": len(after_codes),
},
)
@@ -271,7 +275,9 @@ class AgentFoundationRiskRuleMixin:
continue
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
metadata = (
manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
)
source_ref = str(metadata.get("source_ref") or "").strip()
@@ -347,7 +353,11 @@ class AgentFoundationRiskRuleMixin:
version="v1.0.0",
content=self._platform_risk_rule_markdown(asset, manifest=manifest, file_name=file_name),
content=self._platform_risk_rule_markdown(
asset,
manifest=manifest,
file_name=file_name,
),
content_type=AgentAssetContentType.MARKDOWN.value,
@@ -389,19 +399,25 @@ class AgentFoundationRiskRuleMixin:
config = asset.config_json if isinstance(asset.config_json, dict) else {}
rule_document = config.get("rule_document") if isinstance(config.get("rule_document"), dict) else {}
rule_document = (
config.get("rule_document") if isinstance(config.get("rule_document"), dict) else {}
)
resolved_file_name = file_name or str(rule_document.get("file_name") or "").strip()
evaluator = str(config.get("evaluator") or (manifest or {}).get("evaluator") or "").strip()
ontology_signal = str(config.get("ontology_signal") or (manifest or {}).get("ontology_signal") or "").strip()
ontology_signal = str(
config.get("ontology_signal") or (manifest or {}).get("ontology_signal") or ""
).strip()
source_ref = str(config.get("source_ref") or "").strip()
if not source_ref and isinstance(manifest, dict):
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
metadata = (
manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
)
source_ref = str(metadata.get("source_ref") or "").strip()
@@ -457,7 +473,10 @@ class AgentFoundationRiskRuleMixin:
return AgentFoundationRiskRuleMixin._platform_risk_rule_markdown(
AgentAsset(name="申报地点与票据地点一致", config_json={"evaluator": "location_consistency"}),
AgentAsset(
name="申报地点与票据地点一致",
config_json={"evaluator": "location_consistency"},
),
manifest={