2026-05-22 10:42:31 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
from datetime import UTC, datetime
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
from sqlalchemy import select
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
from app.core.agent_enums import (
|
|
|
|
|
|
AgentAssetContentType,
|
|
|
|
|
|
AgentAssetDomain,
|
|
|
|
|
|
AgentAssetStatus,
|
|
|
|
|
|
AgentAssetType,
|
|
|
|
|
|
AgentReviewStatus,
|
|
|
|
|
|
)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
from app.core.logging import get_logger
|
|
|
|
|
|
from app.models.agent_asset import AgentAsset
|
2026-05-22 10:42:31 +08:00
|
|
|
|
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")
|
|
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
|
EXPENSE_TYPE_SCENARIO_LABELS = {
|
|
|
|
|
|
"travel": "差旅费",
|
|
|
|
|
|
"hotel": "住宿费",
|
|
|
|
|
|
"transport": "交通费",
|
|
|
|
|
|
"meal": "业务招待费",
|
|
|
|
|
|
"meeting": "会务费",
|
|
|
|
|
|
"marketing": "市场推广费",
|
|
|
|
|
|
"office": "办公用品费",
|
|
|
|
|
|
"training": "培训费",
|
|
|
|
|
|
"software": "软件服务费",
|
|
|
|
|
|
"communication": "通信费",
|
|
|
|
|
|
"welfare": "福利费",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 10:42:31 +08:00
|
|
|
|
class AgentFoundationRiskRuleMixin:
|
|
|
|
|
|
def _iter_platform_risk_manifests(self) -> list[tuple[str, dict[str, object]]]:
|
|
|
|
|
|
|
|
|
|
|
|
manager = AgentAssetRuleLibraryManager()
|
|
|
|
|
|
|
|
|
|
|
|
manifests: list[tuple[str, dict[str, object]]] = []
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
for file_name in sorted(
|
|
|
|
|
|
manager.list_rule_library_json_files(library=RISK_RULES_LIBRARY)
|
|
|
|
|
|
):
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
payload = manager.read_rule_library_json(
|
|
|
|
|
|
library=RISK_RULES_LIBRARY,
|
|
|
|
|
|
file_name=file_name,
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
if payload.get("enabled") is False:
|
|
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
if self._is_user_generated_risk_manifest(payload):
|
|
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-05-22 10:42:31 +08:00
|
|
|
|
manifests.append((file_name, payload))
|
|
|
|
|
|
|
|
|
|
|
|
return manifests
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-05-22 10:42:31 +08:00
|
|
|
|
def _resolve_platform_risk_category(manifest: dict[str, object]) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
explicit = str(manifest.get("risk_category") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if explicit:
|
|
|
|
|
|
|
|
|
|
|
|
return explicit
|
|
|
|
|
|
|
|
|
|
|
|
rule_code = str(manifest.get("rule_code") or "").strip().lower()
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
applies_to = (
|
|
|
|
|
|
manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {}
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
domains = {str(item or "").strip().lower() for item in applies_to.get("domains") or []}
|
|
|
|
|
|
|
|
|
|
|
|
expense_types = {
|
|
|
|
|
|
|
|
|
|
|
|
str(item or "").strip().lower() for item in applies_to.get("expense_types") or []
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if rule_code.startswith("risk.invoice."):
|
|
|
|
|
|
|
|
|
|
|
|
return "发票"
|
|
|
|
|
|
|
|
|
|
|
|
if "meal" in domains or "entertainment" in expense_types:
|
|
|
|
|
|
|
|
|
|
|
|
return "餐饮招待"
|
|
|
|
|
|
|
|
|
|
|
|
if "transport" in expense_types or "consecutive_transport" in rule_code:
|
|
|
|
|
|
|
|
|
|
|
|
return "交通出行"
|
|
|
|
|
|
|
|
|
|
|
|
if "office" in expense_types:
|
|
|
|
|
|
|
|
|
|
|
|
return "办公物料"
|
|
|
|
|
|
|
|
|
|
|
|
if "travel" in domains or rule_code.startswith("risk.travel."):
|
|
|
|
|
|
|
|
|
|
|
|
return "差旅"
|
|
|
|
|
|
|
|
|
|
|
|
if rule_code.startswith("risk.expense."):
|
|
|
|
|
|
|
|
|
|
|
|
return "费用科目"
|
|
|
|
|
|
|
|
|
|
|
|
return "通用"
|
|
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _resolve_manifest_expense_types(manifest: dict[str, object]) -> list[str]:
|
|
|
|
|
|
def _collect(value: object) -> list[str]:
|
|
|
|
|
|
if isinstance(value, str):
|
|
|
|
|
|
return [value]
|
|
|
|
|
|
if isinstance(value, (list, tuple, set)):
|
|
|
|
|
|
return [str(item or "").strip() for item in value]
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
candidates: list[str] = []
|
|
|
|
|
|
candidates.extend(_collect(manifest.get("expense_types")))
|
|
|
|
|
|
|
|
|
|
|
|
applies_to = (
|
|
|
|
|
|
manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {}
|
|
|
|
|
|
)
|
|
|
|
|
|
candidates.extend(_collect(applies_to.get("expense_types")))
|
|
|
|
|
|
|
|
|
|
|
|
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
candidates.extend(_collect(metadata.get("expense_types")))
|
|
|
|
|
|
|
|
|
|
|
|
normalized: list[str] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
for item in candidates:
|
|
|
|
|
|
value = item.strip().lower()
|
|
|
|
|
|
if not value or value in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(value)
|
|
|
|
|
|
normalized.append(value)
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _expense_type_scenario_labels(expense_types: list[str]) -> list[str]:
|
|
|
|
|
|
labels: list[str] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
for expense_type in expense_types:
|
|
|
|
|
|
label = EXPENSE_TYPE_SCENARIO_LABELS.get(expense_type)
|
|
|
|
|
|
if not label or label in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(label)
|
|
|
|
|
|
labels.append(label)
|
|
|
|
|
|
return labels
|
|
|
|
|
|
|
2026-05-22 10:42:31 +08:00
|
|
|
|
def _platform_risk_scenario_json(self, manifest: dict[str, object]) -> list[str]:
|
|
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
|
labels = self._expense_type_scenario_labels(self._resolve_manifest_expense_types(manifest))
|
|
|
|
|
|
if labels:
|
|
|
|
|
|
return labels
|
|
|
|
|
|
|
2026-05-22 10:42:31 +08:00
|
|
|
|
category = self._resolve_platform_risk_category(manifest)
|
|
|
|
|
|
|
|
|
|
|
|
return [category] if category else ["通用"]
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
def _platform_risk_config_json(
|
|
|
|
|
|
self, file_name: str, manifest: dict[str, object]
|
|
|
|
|
|
) -> dict[str, object]:
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {}
|
|
|
|
|
|
|
|
|
|
|
|
fail_outcome = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {}
|
|
|
|
|
|
|
|
|
|
|
|
risk_category = self._resolve_platform_risk_category(manifest)
|
|
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
|
config = {
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
"severity": str(fail_outcome.get("severity") or "medium"),
|
|
|
|
|
|
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
|
|
|
|
|
|
"tag": "风险规则",
|
|
|
|
|
|
|
|
|
|
|
|
"detail_mode": "json_risk",
|
|
|
|
|
|
|
|
|
|
|
|
"risk_category": risk_category,
|
|
|
|
|
|
|
|
|
|
|
|
"rule_library": RISK_RULES_LIBRARY,
|
|
|
|
|
|
|
|
|
|
|
|
"rule_document": {
|
|
|
|
|
|
|
|
|
|
|
|
"file_name": file_name,
|
|
|
|
|
|
|
|
|
|
|
|
"storage_key": f"rules/{RISK_RULES_LIBRARY}/{file_name}",
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"ontology_signal": str(manifest.get("ontology_signal") or "").strip(),
|
|
|
|
|
|
|
|
|
|
|
|
"evaluator": str(manifest.get("evaluator") or "").strip(),
|
|
|
|
|
|
|
|
|
|
|
|
"source_ref": (
|
|
|
|
|
|
|
|
|
|
|
|
(manifest.get("metadata") or {}).get("source_ref")
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(manifest.get("metadata"), dict)
|
|
|
|
|
|
|
|
|
|
|
|
else ""
|
|
|
|
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2026-05-26 17:29:35 +08:00
|
|
|
|
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
for key in (
|
|
|
|
|
|
"finance_rule_code",
|
|
|
|
|
|
"finance_rule_sheet",
|
|
|
|
|
|
"business_stage",
|
|
|
|
|
|
"expense_types",
|
|
|
|
|
|
"budget_required",
|
|
|
|
|
|
):
|
|
|
|
|
|
value = manifest.get(key)
|
|
|
|
|
|
if value is None and isinstance(metadata, dict):
|
|
|
|
|
|
value = metadata.get(key)
|
|
|
|
|
|
if value is not None:
|
|
|
|
|
|
config[key] = value
|
|
|
|
|
|
return config
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
def _build_platform_risk_seed_assets(self) -> list[AgentAsset]:
|
|
|
|
|
|
|
|
|
|
|
|
assets: list[AgentAsset] = []
|
|
|
|
|
|
|
|
|
|
|
|
for file_name, manifest in self._iter_platform_risk_manifests():
|
|
|
|
|
|
|
|
|
|
|
|
rule_code = str(manifest.get("rule_code") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if not rule_code:
|
|
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
metadata = (
|
|
|
|
|
|
manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
source_ref = str(metadata.get("source_ref") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
rule_description = str(manifest.get("description") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
assets.append(
|
|
|
|
|
|
|
|
|
|
|
|
AgentAsset(
|
|
|
|
|
|
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
|
|
|
|
|
|
code=rule_code,
|
|
|
|
|
|
|
|
|
|
|
|
name=str(manifest.get("name") or rule_code),
|
|
|
|
|
|
|
|
|
|
|
|
description=rule_description
|
|
|
|
|
|
|
|
|
|
|
|
or f"平台通用风险规则:{source_ref or manifest.get('name') or rule_code}",
|
|
|
|
|
|
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
|
|
|
|
|
|
scenario_json=self._platform_risk_scenario_json(manifest),
|
|
|
|
|
|
|
|
|
|
|
|
owner=str(metadata.get("owner") or "风控与审计部"),
|
|
|
|
|
|
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
|
|
|
|
|
|
published_version="v1.0.0",
|
|
|
|
|
|
|
|
|
|
|
|
working_version="v1.0.0",
|
|
|
|
|
|
|
|
|
|
|
|
config_json=self._platform_risk_config_json(file_name, manifest),
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return assets
|
|
|
|
|
|
|
|
|
|
|
|
def sync_platform_risk_rules_from_library(self) -> int:
|
|
|
|
|
|
|
|
|
|
|
|
existing_codes = set(self.db.scalars(select(AgentAsset.code)).all())
|
|
|
|
|
|
|
|
|
|
|
|
before_count = len(existing_codes)
|
|
|
|
|
|
|
|
|
|
|
|
self._ensure_platform_risk_rules_from_library(existing_codes)
|
2026-05-26 17:29:35 +08:00
|
|
|
|
manifest_codes = {
|
|
|
|
|
|
str(manifest.get("rule_code") or "").strip()
|
|
|
|
|
|
for _, manifest in self._iter_platform_risk_manifests()
|
|
|
|
|
|
if str(manifest.get("rule_code") or "").strip()
|
|
|
|
|
|
}
|
|
|
|
|
|
self._hide_stale_demo_risk_rules(manifest_codes)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
self.db.flush()
|
|
|
|
|
|
|
|
|
|
|
|
after_codes = set(self.db.scalars(select(AgentAsset.code)).all())
|
|
|
|
|
|
|
|
|
|
|
|
synced = max(len(after_codes) - before_count, 0)
|
|
|
|
|
|
|
|
|
|
|
|
manifest_count = len(self._iter_platform_risk_manifests())
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
|
|
|
|
|
|
"Platform risk rules synced from library",
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
extra={
|
|
|
|
|
|
"manifest_count": manifest_count,
|
|
|
|
|
|
"created_count": synced,
|
|
|
|
|
|
"total": len(after_codes),
|
|
|
|
|
|
},
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return manifest_count
|
|
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
|
def _hide_stale_demo_risk_rules(self, manifest_codes: set[str]) -> None:
|
|
|
|
|
|
assets = self.db.scalars(
|
|
|
|
|
|
select(AgentAsset).where(AgentAsset.asset_type == AgentAssetType.RULE.value)
|
|
|
|
|
|
).all()
|
|
|
|
|
|
for asset in assets:
|
|
|
|
|
|
config = asset.config_json if isinstance(asset.config_json, dict) else {}
|
|
|
|
|
|
if config.get("source_ref") != "费用管控 Demo 风险规则库":
|
|
|
|
|
|
continue
|
|
|
|
|
|
if asset.code in manifest_codes:
|
|
|
|
|
|
continue
|
|
|
|
|
|
asset.status = AgentAssetStatus.DISABLED.value
|
|
|
|
|
|
asset.config_json = {
|
|
|
|
|
|
**config,
|
|
|
|
|
|
"enabled": False,
|
|
|
|
|
|
"tag": "废弃风险规则",
|
|
|
|
|
|
"deprecated": True,
|
|
|
|
|
|
"deprecated_reason": "对应风险规则 JSON 已删除,不再参与费用管控 Demo。",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 10:42:31 +08:00
|
|
|
|
def _ensure_platform_risk_rules_from_library(self, existing_codes: set[str]) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
for file_name, manifest in self._iter_platform_risk_manifests():
|
|
|
|
|
|
|
|
|
|
|
|
rule_code = str(manifest.get("rule_code") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if not rule_code:
|
|
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
metadata = (
|
|
|
|
|
|
manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
source_ref = str(metadata.get("source_ref") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
rule_description = str(manifest.get("description") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
config_json = self._platform_risk_config_json(file_name, manifest)
|
|
|
|
|
|
|
|
|
|
|
|
scenario_json = self._platform_risk_scenario_json(manifest)
|
|
|
|
|
|
|
|
|
|
|
|
asset = self.db.scalar(select(AgentAsset).where(AgentAsset.code == rule_code))
|
|
|
|
|
|
|
|
|
|
|
|
if asset is None and rule_code not in existing_codes:
|
|
|
|
|
|
|
|
|
|
|
|
asset = self._create_seed_asset(
|
|
|
|
|
|
|
|
|
|
|
|
asset_type=AgentAssetType.RULE.value,
|
|
|
|
|
|
|
|
|
|
|
|
code=rule_code,
|
|
|
|
|
|
|
|
|
|
|
|
name=str(manifest.get("name") or rule_code),
|
|
|
|
|
|
|
|
|
|
|
|
description=rule_description
|
|
|
|
|
|
|
|
|
|
|
|
or f"平台通用风险规则:{source_ref or manifest.get('name') or rule_code}",
|
|
|
|
|
|
|
|
|
|
|
|
domain=AgentAssetDomain.EXPENSE.value,
|
|
|
|
|
|
|
|
|
|
|
|
scenario_json=scenario_json,
|
|
|
|
|
|
|
|
|
|
|
|
owner=str(metadata.get("owner") or "风控与审计部"),
|
|
|
|
|
|
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
|
|
|
|
|
|
status=AgentAssetStatus.ACTIVE.value,
|
|
|
|
|
|
|
|
|
|
|
|
current_version="v1.0.0",
|
|
|
|
|
|
|
|
|
|
|
|
config_json=config_json,
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if asset is None:
|
|
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if not str(asset.current_version or "").strip():
|
|
|
|
|
|
|
|
|
|
|
|
asset.current_version = "v1.0.0"
|
|
|
|
|
|
|
|
|
|
|
|
if not str(asset.working_version or "").strip():
|
|
|
|
|
|
|
|
|
|
|
|
asset.working_version = asset.current_version
|
|
|
|
|
|
|
|
|
|
|
|
if not str(asset.published_version or "").strip():
|
|
|
|
|
|
|
|
|
|
|
|
asset.published_version = asset.current_version
|
|
|
|
|
|
|
|
|
|
|
|
asset.status = asset.status or AgentAssetStatus.ACTIVE.value
|
|
|
|
|
|
|
|
|
|
|
|
asset.name = str(manifest.get("name") or asset.name or rule_code)
|
|
|
|
|
|
|
|
|
|
|
|
if rule_description:
|
|
|
|
|
|
|
|
|
|
|
|
asset.description = rule_description
|
|
|
|
|
|
|
|
|
|
|
|
asset.config_json = config_json
|
|
|
|
|
|
|
|
|
|
|
|
asset.scenario_json = scenario_json
|
|
|
|
|
|
|
|
|
|
|
|
self._ensure_asset_version(
|
|
|
|
|
|
|
|
|
|
|
|
asset,
|
|
|
|
|
|
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
content=self._platform_risk_rule_markdown(
|
|
|
|
|
|
asset,
|
|
|
|
|
|
manifest=manifest,
|
|
|
|
|
|
file_name=file_name,
|
|
|
|
|
|
),
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
content_type=AgentAssetContentType.MARKDOWN.value,
|
|
|
|
|
|
|
|
|
|
|
|
change_note=f"平台通用风险规则:{asset.name}",
|
|
|
|
|
|
|
|
|
|
|
|
created_by="系统初始化",
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._ensure_asset_review(
|
|
|
|
|
|
|
|
|
|
|
|
asset,
|
|
|
|
|
|
|
|
|
|
|
|
version="v1.0.0",
|
|
|
|
|
|
|
|
|
|
|
|
reviewer="顾承宇",
|
|
|
|
|
|
|
|
|
|
|
|
review_status=AgentReviewStatus.APPROVED.value,
|
|
|
|
|
|
|
|
|
|
|
|
review_note="平台内置风险规则,供提交验审与风险问答共用。",
|
|
|
|
|
|
|
|
|
|
|
|
reviewed_at=datetime.now(UTC),
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
|
|
|
|
def _platform_risk_rule_markdown(
|
|
|
|
|
|
|
|
|
|
|
|
asset: AgentAsset,
|
|
|
|
|
|
|
|
|
|
|
|
*,
|
|
|
|
|
|
|
|
|
|
|
|
manifest: dict[str, object] | None = None,
|
|
|
|
|
|
|
|
|
|
|
|
file_name: str = "",
|
|
|
|
|
|
|
|
|
|
|
|
) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
config = asset.config_json if isinstance(asset.config_json, dict) else {}
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
rule_document = (
|
|
|
|
|
|
config.get("rule_document") if isinstance(config.get("rule_document"), dict) else {}
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
ontology_signal = str(
|
|
|
|
|
|
config.get("ontology_signal") or (manifest or {}).get("ontology_signal") or ""
|
|
|
|
|
|
).strip()
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
source_ref = str(config.get("source_ref") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if not source_ref and isinstance(manifest, dict):
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
metadata = (
|
|
|
|
|
|
manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
|
|
|
|
|
)
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
source_ref = str(metadata.get("source_ref") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
lines = [
|
|
|
|
|
|
|
|
|
|
|
|
f"# {asset.name}",
|
|
|
|
|
|
|
|
|
|
|
|
"",
|
|
|
|
|
|
|
|
|
|
|
|
"## 规则类型",
|
|
|
|
|
|
|
|
|
|
|
|
"",
|
|
|
|
|
|
|
|
|
|
|
|
"- 平台内置通用风险规则(`json_risk`)",
|
|
|
|
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if evaluator:
|
|
|
|
|
|
|
|
|
|
|
|
lines.append(f"- 检查器:`{evaluator}`")
|
|
|
|
|
|
|
|
|
|
|
|
if ontology_signal:
|
|
|
|
|
|
|
|
|
|
|
|
lines.append(f"- 本体信号:`{ontology_signal}`")
|
|
|
|
|
|
|
|
|
|
|
|
if source_ref:
|
|
|
|
|
|
|
|
|
|
|
|
lines.extend(["", "## 来源", "", f"- {source_ref}"])
|
|
|
|
|
|
|
|
|
|
|
|
if resolved_file_name:
|
|
|
|
|
|
|
|
|
|
|
|
lines.extend(
|
|
|
|
|
|
|
|
|
|
|
|
[
|
|
|
|
|
|
|
|
|
|
|
|
"",
|
|
|
|
|
|
|
|
|
|
|
|
"## 配置文件",
|
|
|
|
|
|
|
|
|
|
|
|
"",
|
|
|
|
|
|
|
|
|
|
|
|
f"- `rules/{RISK_RULES_LIBRARY}/{resolved_file_name}`",
|
|
|
|
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
|
|
|
|
def _platform_destination_location_risk_markdown() -> str:
|
|
|
|
|
|
|
|
|
|
|
|
return AgentFoundationRiskRuleMixin._platform_risk_rule_markdown(
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
AgentAsset(
|
|
|
|
|
|
name="申报地点与票据地点一致",
|
|
|
|
|
|
config_json={"evaluator": "location_consistency"},
|
|
|
|
|
|
),
|
2026-05-22 10:42:31 +08:00
|
|
|
|
|
|
|
|
|
|
manifest={
|
|
|
|
|
|
|
|
|
|
|
|
"evaluator": "location_consistency",
|
|
|
|
|
|
|
|
|
|
|
|
"ontology_signal": "location_mismatch",
|
|
|
|
|
|
|
|
|
|
|
|
"metadata": {"source_ref": "常用risk.txt / 一、出差类 / 行程不符"},
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
file_name=PLATFORM_DESTINATION_LOCATION_RULE_FILENAME,
|
|
|
|
|
|
|
|
|
|
|
|
)
|