- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
216 lines
8.9 KiB
Python
216 lines
8.9 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from app.core.agent_enums import AgentAssetStatus, AgentAssetType, AgentReviewStatus
|
|
from app.models.agent_asset import AgentAsset, AgentAssetReview
|
|
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
|
|
from app.services.risk_rule_manifest_normalizer import normalize_risk_rule_manifest
|
|
|
|
|
|
class AgentAssetRiskRulePublishMixin:
|
|
"""风险规则发布逻辑,支持普通待审核版本和已上线规则修订版本。"""
|
|
|
|
def publish_risk_rule(
|
|
self,
|
|
asset_id: str,
|
|
*,
|
|
actor: str,
|
|
request_id: str | None = None,
|
|
) -> AgentAsset:
|
|
asset = self._resolve_asset(asset_id)
|
|
self._require_json_risk_asset(asset)
|
|
revision = self._resolve_publishable_revision(asset)
|
|
if revision is not None:
|
|
return self._publish_revision(asset, revision, actor=actor, request_id=request_id)
|
|
return self._publish_reviewed_working_version(asset, actor=actor, request_id=request_id)
|
|
|
|
def _publish_reviewed_working_version(
|
|
self,
|
|
asset: AgentAsset,
|
|
*,
|
|
actor: str,
|
|
request_id: str | None,
|
|
) -> AgentAsset:
|
|
version = self._resolve_target_version(asset, None)
|
|
if asset.status != AgentAssetStatus.REVIEW.value:
|
|
raise ValueError("只有待审核风险规则可以发布上线。")
|
|
if not self.get_latest_risk_rule_test_summary(asset, version=version).test_passed:
|
|
raise PermissionError("当前规则版本尚未完成测试通过确认,不能发布。")
|
|
|
|
before = self._asset_snapshot(asset)
|
|
self._ensure_approved_review(asset, version=version, actor=actor, note="发布上线前审核通过。")
|
|
asset.reviewer = actor
|
|
asset.published_version = version
|
|
asset.status = AgentAssetStatus.ACTIVE.value
|
|
self.db.add(asset)
|
|
self.db.commit()
|
|
self.audit_service.log_action(
|
|
actor=actor,
|
|
action="publish_agent_asset",
|
|
resource_type=AgentAssetType.RULE.value,
|
|
resource_id=asset.id,
|
|
before_json=before,
|
|
after_json=self._asset_snapshot(asset),
|
|
request_id=request_id,
|
|
)
|
|
return self._refresh_asset(asset.id)
|
|
|
|
def _publish_revision(
|
|
self,
|
|
asset: AgentAsset,
|
|
revision: dict[str, Any],
|
|
*,
|
|
actor: str,
|
|
request_id: str | None,
|
|
) -> AgentAsset:
|
|
version = str(revision.get("version") or "").strip()
|
|
if not self.get_latest_risk_rule_test_summary(asset, version=version).test_passed:
|
|
raise PermissionError("当前修订版本尚未完成测试通过确认,不能发布。")
|
|
|
|
rule_document = revision.get("rule_document") if isinstance(revision.get("rule_document"), dict) else {}
|
|
file_name = str(rule_document.get("file_name") or "").strip()
|
|
if not file_name:
|
|
raise ValueError("修订版本尚未生成可发布的 JSON 规则文件。")
|
|
|
|
before = self._asset_snapshot(asset)
|
|
manifest = self.rule_library_manager.read_rule_library_json(
|
|
library=RISK_RULES_LIBRARY,
|
|
file_name=file_name,
|
|
)
|
|
manifest = normalize_risk_rule_manifest(manifest)
|
|
manifest["enabled"] = True
|
|
self.rule_library_manager.write_rule_library_json(
|
|
library=RISK_RULES_LIBRARY,
|
|
file_name=file_name,
|
|
payload=manifest,
|
|
)
|
|
|
|
config = dict(asset.config_json or {})
|
|
previous_rule_document = config.get("rule_document") if isinstance(config.get("rule_document"), dict) else {}
|
|
published_at = datetime.now(UTC).isoformat()
|
|
history = list(config.get("revision_history") if isinstance(config.get("revision_history"), list) else [])
|
|
history.insert(
|
|
0,
|
|
{
|
|
"version": version,
|
|
"base_version": revision.get("base_version"),
|
|
"change_reason": revision.get("change_reason"),
|
|
"published_by": actor,
|
|
"published_at": published_at,
|
|
"previous_rule_document": previous_rule_document,
|
|
"rule_document": rule_document,
|
|
},
|
|
)
|
|
config.update(self._config_from_published_manifest(manifest, rule_document))
|
|
config["revision_history"] = history[:20]
|
|
config.pop("revision_draft", None)
|
|
config["last_operation"] = {
|
|
"action": "publish_revision",
|
|
"actor": actor,
|
|
"at": published_at,
|
|
"target_version": version,
|
|
}
|
|
|
|
asset.name = str(manifest.get("name") or asset.name)
|
|
asset.description = str(manifest.get("description") or asset.description)
|
|
risk_category = str(manifest.get("risk_category") or "").strip()
|
|
if risk_category:
|
|
asset.scenario_json = [risk_category]
|
|
asset.config_json = config
|
|
asset.current_version = version
|
|
asset.working_version = version
|
|
asset.published_version = version
|
|
asset.reviewer = actor
|
|
asset.status = AgentAssetStatus.ACTIVE.value
|
|
self._ensure_approved_review(asset, version=version, actor=actor, note="修订版本发布上线。")
|
|
self.db.add(asset)
|
|
self.db.commit()
|
|
self.audit_service.log_action(
|
|
actor=actor,
|
|
action="publish_risk_rule_revision",
|
|
resource_type=AgentAssetType.RULE.value,
|
|
resource_id=asset.id,
|
|
before_json=before,
|
|
after_json=self._asset_snapshot(asset),
|
|
request_id=request_id,
|
|
)
|
|
return self._refresh_asset(asset.id)
|
|
|
|
def _resolve_publishable_revision(self, asset: AgentAsset) -> dict[str, Any] | None:
|
|
config = dict(asset.config_json or {})
|
|
revision = config.get("revision_draft")
|
|
if not isinstance(revision, dict):
|
|
return None
|
|
version = str(revision.get("version") or "").strip()
|
|
if not version or version != str(asset.working_version or "").strip():
|
|
return None
|
|
if version == str(asset.published_version or "").strip():
|
|
return None
|
|
if revision.get("generation_status") != "completed":
|
|
raise ValueError("修订版本尚未重新生成,不能发布上线。")
|
|
return dict(revision)
|
|
|
|
def _ensure_approved_review(
|
|
self,
|
|
asset: AgentAsset,
|
|
*,
|
|
version: str,
|
|
actor: str,
|
|
note: str,
|
|
) -> None:
|
|
approved_review = self.repository.get_review(
|
|
asset.id, version, AgentReviewStatus.APPROVED.value
|
|
)
|
|
if approved_review is not None:
|
|
return
|
|
self.db.add(
|
|
AgentAssetReview(
|
|
asset_id=asset.id,
|
|
version=version,
|
|
reviewer=actor,
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
review_note=note,
|
|
reviewed_at=datetime.now(UTC),
|
|
)
|
|
)
|
|
|
|
@staticmethod
|
|
def _config_from_published_manifest(
|
|
manifest: dict[str, Any],
|
|
rule_document: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
risk_score_detail = metadata.get("risk_score_detail") if isinstance(metadata.get("risk_score_detail"), dict) else {}
|
|
risk_level = str(metadata.get("risk_level") or manifest.get("outcomes", {}).get("fail", {}).get("severity") or "medium")
|
|
risk_score = int(metadata.get("risk_score") or manifest.get("outcomes", {}).get("fail", {}).get("risk_score") or 0)
|
|
return {
|
|
"severity": risk_level,
|
|
"risk_score": risk_score,
|
|
"risk_level": risk_level,
|
|
"risk_level_label": metadata.get("risk_level_label"),
|
|
"risk_score_detail": risk_score_detail,
|
|
"enabled": True,
|
|
"requires_attachment": bool(metadata.get("requires_attachment") or manifest.get("requires_attachment")),
|
|
"detail_mode": "json_risk",
|
|
"business_stage": metadata.get("business_stage"),
|
|
"business_stage_label": metadata.get("business_stage_label"),
|
|
"expense_category": metadata.get("expense_category"),
|
|
"expense_category_label": metadata.get("expense_category_label"),
|
|
"risk_category": manifest.get("risk_category"),
|
|
"rule_library": RISK_RULES_LIBRARY,
|
|
"rule_document": rule_document,
|
|
"ontology_signal": manifest.get("ontology_signal"),
|
|
"evaluator": manifest.get("evaluator"),
|
|
"generated_by": "natural_language",
|
|
"source_ref": "自然语言风险规则",
|
|
"flow_diagram_svg": manifest.get("flow_diagram_svg"),
|
|
}
|
|
|
|
def _refresh_asset(self, asset_id: str) -> AgentAsset:
|
|
refreshed = self.repository.get(asset_id)
|
|
if refreshed is None:
|
|
raise LookupError("Asset not found")
|
|
return refreshed
|