You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
# backend/app/approvals/service.py
|
|
# 큐 분기(derive_queue = 원본 approve.jsx initStatus) + 상태 전이.
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
from ..automation.event_bus import bus
|
|
from ..automation.events import APPROVAL_EXECUTED, APPROVAL_UNDONE
|
|
from ..models import (
|
|
Approval,
|
|
ApprovalLog,
|
|
ApprovalStatus,
|
|
AutonomyLevel,
|
|
AutonomySetting,
|
|
RiskLevel,
|
|
)
|
|
|
|
_LEVELS = (
|
|
AutonomyLevel.approval_first.value,
|
|
AutonomyLevel.mixed.value,
|
|
AutonomyLevel.full_auto.value,
|
|
)
|
|
|
|
|
|
def get_autonomy(s: Session) -> str:
|
|
row = s.get(AutonomySetting, "default")
|
|
if not row:
|
|
row = AutonomySetting(id="default", level=AutonomyLevel.mixed.value)
|
|
s.add(row)
|
|
s.commit()
|
|
s.refresh(row)
|
|
return row.level
|
|
|
|
|
|
def set_autonomy(s: Session, level: str) -> str:
|
|
if level not in _LEVELS:
|
|
raise ValueError("invalid autonomy level")
|
|
row = s.get(AutonomySetting, "default") or AutonomySetting(id="default")
|
|
row.level = level
|
|
row.updated_at = datetime.now(UTC)
|
|
s.add(row)
|
|
s.commit()
|
|
return level
|
|
|
|
|
|
def derive_status(level: str, risk: str) -> str:
|
|
"""원본 approve.jsx initStatus(autonomy) 이식.
|
|
full_auto: 전부 executed. mixed: low=executed, high=pending. approval_first: 전부 pending."""
|
|
if level == AutonomyLevel.full_auto.value:
|
|
return ApprovalStatus.executed.value
|
|
if level == AutonomyLevel.mixed.value:
|
|
return (
|
|
ApprovalStatus.executed.value
|
|
if risk == RiskLevel.low.value
|
|
else ApprovalStatus.pending.value
|
|
)
|
|
return ApprovalStatus.pending.value # approval_first
|
|
|
|
|
|
def derive_queue(s: Session, user_id: str | None = None) -> dict:
|
|
"""자율성 레벨로 모든 approval 의 표시 상태를 파생.
|
|
단, 사용자가 명시적으로 undone 한 것은 pending, 엔진이 실제 실행한 건은 executed 로 고정.
|
|
user_id 가 주어지면 소유자 스코프(phase-15)."""
|
|
level = get_autonomy(s)
|
|
stmt = select(Approval).order_by(Approval.sort_order)
|
|
if user_id is not None:
|
|
stmt = stmt.where(Approval.user_id == user_id)
|
|
rows = s.exec(stmt).all()
|
|
pending, done = [], []
|
|
for a in rows:
|
|
if a.status == ApprovalStatus.undone.value: # 사용자가 되돌림 → 대기로
|
|
eff = ApprovalStatus.pending.value
|
|
elif a.status == ApprovalStatus.executed.value and a.executed_at:
|
|
eff = ApprovalStatus.executed.value # 사용자/엔진이 실제 실행한 건은 고정
|
|
else:
|
|
eff = derive_status(level, a.risk) # 시드/표시용은 레벨로 파생
|
|
(done if eff == ApprovalStatus.executed.value else pending).append(a)
|
|
return {"level": level, "pending": pending, "done": done}
|
|
|
|
|
|
def approve(s: Session, aid: str) -> Approval:
|
|
a = s.get(Approval, aid)
|
|
a.status = ApprovalStatus.executed.value
|
|
a.executed_at = datetime.now(UTC)
|
|
s.add(a)
|
|
s.commit()
|
|
s.refresh(a)
|
|
s.add(ApprovalLog(id="alog-" + aid + "-x", time="방금", text=a.title, approval_id=aid))
|
|
s.commit()
|
|
bus.publish(APPROVAL_EXECUTED, {"approval_id": aid, "risk": a.risk, "title": a.title})
|
|
return a
|
|
|
|
|
|
def undo(s: Session, aid: str) -> Approval:
|
|
a = s.get(Approval, aid)
|
|
a.status = ApprovalStatus.undone.value
|
|
a.undone_at = datetime.now(UTC)
|
|
s.add(a)
|
|
s.commit()
|
|
s.refresh(a)
|
|
bus.publish(APPROVAL_UNDONE, {"approval_id": aid, "title": a.title})
|
|
return a
|
|
|
|
|
|
def approve_all(s: Session) -> int:
|
|
q = derive_queue(s)
|
|
n = 0
|
|
for a in q["pending"]:
|
|
approve(s, a.id)
|
|
n += 1
|
|
return n
|