# ============================================================ # 治理 API(moduleId: gateway-governance-api, 可重生 ✅) # R71.6:约束/自动化规则 CRUD + 启停 + 规则审计轨迹(/api/governance) # 与审计统计聚合(/api/gov/audit/stats)。 # 前缀建议:/api/governance(规则管理)与 /api/gov(审计统计)。 # ============================================================ from __future__ import annotations from typing import Annotated, Any from fastapi import APIRouter, Depends, HTTPException, Query, status from pydantic import BaseModel, ConfigDict, Field from server.aps_domain.governance_rules import ( RULE_ID_PATTERN, GovernanceRuleError, GovernanceRuleService, ) from server.auth.context import get_identity router = APIRouter(prefix="/api/governance", tags=["governance"]) gov_router = APIRouter(prefix="/api/gov", tags=["governance-audit"]) class RuleCreateRequest(BaseModel): model_config = ConfigDict(extra="forbid") ruleId: str | None = Field(default=None, pattern=RULE_ID_PATTERN.pattern) roomId: str = Field(min_length=1, max_length=128) trigger: dict[str, Any] gear: str = Field(default="G1", pattern="^G[0-4]$") action: str = "notify" guardrails: list[str] = Field(default_factory=list) enabled: bool = True description: str = "" params: dict[str, Any] = Field(default_factory=dict) class RuleUpdateRequest(BaseModel): model_config = ConfigDict(extra="forbid") ruleId: str | None = Field(default=None, pattern=RULE_ID_PATTERN.pattern) roomId: str | None = Field(default=None, min_length=1, max_length=128) trigger: dict[str, Any] | None = None gear: str | None = Field(default=None, pattern="^G[0-4]$") action: str | None = None guardrails: list[str] | None = None enabled: bool | None = None description: str | None = None params: dict[str, Any] | None = None class RuleView(BaseModel): ruleId: str roomId: str trigger: dict[str, Any] gear: str action: str guardrails: list[str] enabled: bool paused: bool = False description: str params: dict[str, Any] class RuleEnvelope(BaseModel): rule: RuleView class RuleListEnvelope(BaseModel): rules: list[RuleView] class RuleDeleteEnvelope(BaseModel): deleted: bool ruleId: str class RuleStageEnvelope(BaseModel): message: str block: dict[str, Any] class RuleAuditEnvelope(BaseModel): ruleId: str total: int events: list[dict[str, Any]] class AuditStatsEnvelope(BaseModel): ok: bool source: str total: int filtered: int window: dict[str, Any] byModule: list[dict[str, Any]] byAction: list[dict[str, Any]] byResult: list[dict[str, Any]] trend: list[dict[str, Any]] rows: list[dict[str, Any]] def get_governance_service() -> GovernanceRuleService: """默认服务接入网关既有 AutomationRuntime 单例与当前 store。""" return GovernanceRuleService() ServiceDep = Annotated[GovernanceRuleService, Depends(get_governance_service)] def _raise_governance_error(exc: GovernanceRuleError) -> None: if exc.code == "RULE_NOT_FOUND": http_code = status.HTTP_404_NOT_FOUND elif exc.code in ("RULE_EXISTS", "RULE_ID_MISMATCH", "RULE_CONFLICT"): http_code = status.HTTP_409_CONFLICT elif exc.code in ("INVALID_GRANULARITY", "INVALID_TIME", "INVALID_TIME_WINDOW"): http_code = status.HTTP_400_BAD_REQUEST elif exc.code == "PERSISTENCE_FAILED": http_code = status.HTTP_503_SERVICE_UNAVAILABLE else: http_code = 422 raise HTTPException( status_code=http_code, detail={"code": exc.code, "message": str(exc)}, ) from exc def _stage_governance_change( service: GovernanceRuleService, action: str, payload: dict[str, Any], ) -> RuleStageEnvelope: identity = get_identity(required=True) try: block = service.stage_change( action, payload, session_id=f"governance:{identity.user_id}", ) except GovernanceRuleError as exc: _raise_governance_error(exc) except PermissionError as exc: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc return RuleStageEnvelope( message="治理规则变更已进入 P2 确认队列,确认前不会写入规则状态。", block=block.model_dump(), ) @router.get("/rules", response_model=RuleListEnvelope) def list_governance_rules( service: ServiceDep, enabled: bool | None = None, ) -> RuleListEnvelope: """规则清单(可按启用状态过滤)。""" return RuleListEnvelope(rules=service.list_rules(enabled=enabled)) @router.get("/rules/{rule_id}", response_model=RuleEnvelope) def get_governance_rule( rule_id: str, service: ServiceDep, ) -> RuleEnvelope: """单条规则详情。""" try: return RuleEnvelope(rule=service.get_rule(rule_id)) except GovernanceRuleError as exc: _raise_governance_error(exc) @router.post("/rules", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED) def create_governance_rule( request: RuleCreateRequest, service: ServiceDep, ) -> RuleStageEnvelope: """创建规则 P2 出卡;确认前不写规则注册表。""" return _stage_governance_change( service, "governance.rule.create", request.model_dump(), ) @router.put("/rules/{rule_id}", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED) def update_governance_rule( rule_id: str, request: RuleUpdateRequest, service: ServiceDep, ) -> RuleStageEnvelope: """更新规则 P2 出卡;确认前不替换规则定义。""" return _stage_governance_change( service, "governance.rule.update", {"ruleId": rule_id, "patch": request.model_dump(exclude_none=True)}, ) @router.delete("/rules/{rule_id}", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED) def delete_governance_rule( rule_id: str, service: ServiceDep, ) -> RuleStageEnvelope: """删除规则 P2 出卡;确认前不移除规则。""" return _stage_governance_change( service, "governance.rule.delete", {"ruleId": rule_id}, ) @router.post("/rules/{rule_id}/enable", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED) def enable_governance_rule( rule_id: str, service: ServiceDep, ) -> RuleStageEnvelope: """启用规则 P2 出卡;确认前不参与 tick。""" return _stage_governance_change( service, "governance.rule.enable", {"ruleId": rule_id}, ) @router.post("/rules/{rule_id}/disable", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED) def disable_governance_rule( rule_id: str, service: ServiceDep, ) -> RuleStageEnvelope: """停用规则 P2 出卡;确认前保持原启用状态。""" return _stage_governance_change( service, "governance.rule.disable", {"ruleId": rule_id}, ) @router.get("/rules/{rule_id}/audit", response_model=RuleAuditEnvelope) def governance_rule_audit( rule_id: str, service: ServiceDep, limit: int = Query(default=50, ge=1, le=1000), ) -> RuleAuditEnvelope: """规则审计轨迹:target 命中该规则的全部审计事件。""" return RuleAuditEnvelope(**service.rule_audit(rule_id, limit=limit)) @gov_router.get("/audit/stats", response_model=AuditStatsEnvelope) def governance_audit_stats( service: ServiceDep, startTime: str | None = None, endTime: str | None = None, module: str | None = None, action: str | None = None, result: str | None = None, granularity: str = Query(default="day", pattern="^(day|hour)$"), ) -> AuditStatsEnvelope: """审计量聚合:按模块/动作/结果/时间窗口统计(数据源复用 audit ledger)。""" try: return AuditStatsEnvelope(**service.audit_stats( start_time=startTime, end_time=endTime, module=module, action=action, result=result, granularity=granularity, )) except GovernanceRuleError as exc: _raise_governance_error(exc)