"""场景插件框架:统一的场景基类与产出物(A 打底)。 每个审计场景实现一个 BaseScenario 子类,自包含"检测→评分→产出线索草稿", 并通过 @register_scenario 注册。扫描调度器(engines/scan.run_scenarios)统一遍历 执行并落库,新增场景无需改动调度器、API 与前端。 """ from __future__ import annotations import uuid from abc import ABC, abstractmethod from dataclasses import dataclass, field from sqlalchemy.orm import Session @dataclass class ClueDraft: """场景产出的线索草稿:仅承载"内容",落库由调度器统一完成。 风险域、场景码等元数据来自场景类属性;模型/数据版本由调度器注入。 """ score: float rationale: str evidence: dict subjects: dict = field(default_factory=dict) amount_involved: float | None = None title: str | None = None # 覆盖场景默认标题;为 None 时用 BaseScenario.title @dataclass class ScanOutcome: """单个场景一次扫描的结果:扫描覆盖数(证明全量性)+ 线索草稿列表。""" scanned_count: int drafts: list[ClueDraft] = field(default_factory=list) class BaseScenario(ABC): """审计场景抽象基类。 子类通过类属性声明元数据(场景码、标题、风险域、前端展示名、阈值), 并实现 scan() 完成检测与评分。命中阈值的草稿由调度器统一生成线索。 """ # 场景码(如 "R8"),全局唯一,注册表按此索引 code: str = "" # 默认线索标题(草稿可覆盖) title: str = "" # 风险域(如 "收入"/"成本") risk_domain: str = "" # 前端展示名(替代 labels.ts 中写死的 scenarioLabel) label: str = "" # 评分阈值:草稿 score >= 阈值才生成线索 score_threshold: float = 0.5 # 规则版本(配置驱动场景可用于追溯;代码场景一般为 None) rule_version: str | None = None @abstractmethod def scan( self, session: Session, *, data_version_id: uuid.UUID | None = None, ) -> ScanOutcome: """执行检测与评分,返回扫描覆盖数与命中草稿。 Args: session: 数据库会话(如需自取数) data_version_id: 当前数据版本 ID(用于线索可追溯) Returns: ScanOutcome:scanned_count 体现全量覆盖,drafts 为候选线索 """ ...