Files
company-ai-platform/app/modules/approvals/service.py
JiuContinent aa81fc5321 ```
feat(ai_agent): 完善AI适配器和服务功能

- 添加OpenClaw和Hermes健康检查接口
- 实现OpenClaw工具调用功能
- 重构AI适配器使用常量定义
- 增加AI技能系统支持
- 更新配置文件中的默认模型提供者设置

refactor(scheduler): 使用常量替换硬编码值

- 将硬编码的actor值替换为ActorValue常量
- 将receive_id_type替换为FeishuReceiveIdType枚举

refactor(audit): 统一审计日志常量使用

- 将硬编码的actor、source、risk_level等值替换为对应常量
- 更新审核服务中的状态和操作常量引用

refactor(approvals): 标准化审批模块常量使用

- 将applicant默认值替换为ActorValue.API常量
- 使用ApprovalStatus常量替代硬编码状态值
- 更新审核操作常量引用
```
2026-07-06 00:02:03 +08:00

119 lines
4.3 KiB
Python

import json
import uuid
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.modules.approvals.constants import ApprovalActionValue, ApprovalStatus
from app.modules.approvals.models import ApprovalRequest
from app.modules.approvals.schemas import ApprovalCreate
from app.modules.audit.constants import AuditAction, AuditRiskLevel, AuditSource
from app.modules.audit.schemas import AuditLogCreate
from app.modules.audit.service import AuditService
class ApprovalService:
"""Create, decide, and validate approval tickets for guarded actions."""
def __init__(self, db: Session):
self.db = db
self.audit = AuditService(db)
def create(self, payload: ApprovalCreate) -> ApprovalRequest:
ticket = ApprovalRequest(
ticket_id=f"APR-{uuid.uuid4().hex[:12].upper()}",
domain=payload.domain,
record_id=payload.record_id,
action=payload.action,
applicant=payload.applicant,
reason=payload.reason,
payload=json.dumps(payload.payload, ensure_ascii=False, default=str),
)
self.db.add(ticket)
self.db.commit()
self.db.refresh(ticket)
self.audit.log(
AuditLogCreate(
actor=payload.applicant,
source=AuditSource.APPROVAL,
action=AuditAction.APPROVAL_CREATE,
target_type=payload.domain,
target_id=payload.record_id,
risk_level=AuditRiskLevel.MEDIUM,
request_payload=payload.model_dump(),
response_payload={"ticket_id": ticket.ticket_id, "status": ticket.status},
)
)
return ticket
def list(self, status_filter: str | None = None, limit: int = 100) -> list[ApprovalRequest]:
stmt = select(ApprovalRequest).order_by(ApprovalRequest.id.desc()).limit(min(limit, 500))
if status_filter:
stmt = stmt.where(ApprovalRequest.status == status_filter)
return list(self.db.execute(stmt).scalars())
def get_by_ticket(self, ticket_id: str) -> ApprovalRequest:
ticket = self.db.execute(
select(ApprovalRequest).where(ApprovalRequest.ticket_id == ticket_id)
).scalar_one_or_none()
if ticket is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Approval ticket not found",
)
return ticket
def decide(
self,
ticket_id: str,
approver: str,
approved: bool,
comment: str | None,
) -> ApprovalRequest:
ticket = self.get_by_ticket(ticket_id)
if ticket.status != ApprovalStatus.PENDING:
raise HTTPException(status_code=409, detail="Approval ticket already decided")
ticket.status = ApprovalStatus.APPROVED if approved else ApprovalStatus.REJECTED
ticket.approver = approver
ticket.decision_comment = comment
from datetime import datetime
ticket.decided_at = datetime.utcnow()
self.db.commit()
self.db.refresh(ticket)
self.audit.log(
AuditLogCreate(
actor=approver,
source=AuditSource.APPROVAL,
action=AuditAction.APPROVAL_APPROVE if approved else AuditAction.APPROVAL_REJECT,
target_type=ticket.domain,
target_id=ticket.record_id,
risk_level=AuditRiskLevel.HIGH,
request_payload={"ticket_id": ticket_id, "comment": comment},
response_payload={"status": ticket.status},
)
)
return ticket
def is_approved_for(
self,
ticket_id: str,
domain: str,
record_id: str | int | None,
action: str,
) -> bool:
ticket = self.get_by_ticket(ticket_id)
if ticket.status != ApprovalStatus.APPROVED:
return False
if ticket.domain != domain:
return False
if ticket.record_id and record_id is not None and str(ticket.record_id) != str(record_id):
return False
return ticket.action in {
action,
ApprovalActionValue.UPDATE,
f"{ApprovalActionValue.UPDATE}:{domain}",
ApprovalActionValue.WILDCARD,
}