269 lines
8.1 KiB
Python
269 lines
8.1 KiB
Python
# ============================================================
|
||
# 治理 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)
|