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