refactor(approval): 将ApprovalEngine迁移到独立服务模块
删除原approval通道下的engine.py文件,将ApprovalEngine导入路径调整到channel_approval_service服务模块,统一审批引擎的代码组织方式
This commit is contained in:
parent
f77d2b746d
commit
e200457519
@ -1,5 +1,5 @@
|
|||||||
from yuxi.channel.approval.delivery import DeliveryResult, deliver_approval_request
|
from yuxi.channel.approval.delivery import DeliveryResult, deliver_approval_request
|
||||||
from yuxi.channel.approval.engine import ApprovalEngine
|
from yuxi.services.channel_approval_service import ApprovalEngine
|
||||||
from yuxi.channel.approval.native_runtime import (
|
from yuxi.channel.approval.native_runtime import (
|
||||||
NativeApprovalCard,
|
NativeApprovalCard,
|
||||||
NativeApprovalRenderer,
|
NativeApprovalRenderer,
|
||||||
|
|||||||
@ -1,272 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
from yuxi.channel.protocols import (
|
|
||||||
ApprovalAction,
|
|
||||||
ApprovalDecision,
|
|
||||||
ApprovalProtocol,
|
|
||||||
ApprovalRequest,
|
|
||||||
ApprovalResult,
|
|
||||||
)
|
|
||||||
from yuxi.channel.plugins.registry import ChannelPluginRegistry
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_TERMINAL_DECISIONS = frozenset({
|
|
||||||
ApprovalDecision.APPROVED,
|
|
||||||
ApprovalDecision.DENIED,
|
|
||||||
ApprovalDecision.CANCELLED,
|
|
||||||
ApprovalDecision.EXPIRED,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class ApprovalEngine:
|
|
||||||
def __init__(self):
|
|
||||||
self._pending: dict[str, ApprovalRequest] = {}
|
|
||||||
self._results: dict[str, ApprovalResult] = {}
|
|
||||||
self._events: dict[str, asyncio.Event] = {}
|
|
||||||
self._cleanup_task: asyncio.Task | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pending_count(self) -> int:
|
|
||||||
return len(self._pending)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def results_count(self) -> int:
|
|
||||||
return len(self._results)
|
|
||||||
|
|
||||||
def get_pending_request(self, request_id: str) -> ApprovalRequest | None:
|
|
||||||
return self._pending.get(request_id)
|
|
||||||
|
|
||||||
def get_result(self, request_id: str) -> ApprovalResult | None:
|
|
||||||
return self._results.get(request_id)
|
|
||||||
|
|
||||||
def _is_terminal(self, request_id: str) -> bool:
|
|
||||||
existing = self._results.get(request_id)
|
|
||||||
return existing is not None and existing.decision in _TERMINAL_DECISIONS
|
|
||||||
|
|
||||||
def cancel_request(self, request_id: str, reason: str = "已取消") -> bool:
|
|
||||||
existing = self._results.get(request_id)
|
|
||||||
if existing is not None:
|
|
||||||
logger.warning(
|
|
||||||
"审批请求已达终态,无法取消: req=%s, current=%s",
|
|
||||||
request_id,
|
|
||||||
existing.decision.value,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
if request_id not in self._pending:
|
|
||||||
return False
|
|
||||||
del self._pending[request_id]
|
|
||||||
result = ApprovalResult(
|
|
||||||
request_id=request_id,
|
|
||||||
decision=ApprovalDecision.CANCELLED,
|
|
||||||
reason=reason,
|
|
||||||
)
|
|
||||||
self._results[request_id] = result
|
|
||||||
self._signal(request_id)
|
|
||||||
logger.info("审批请求已取消: req=%s, reason=%s", request_id, reason)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def register_decision(
|
|
||||||
self,
|
|
||||||
request_id: str,
|
|
||||||
decision: ApprovalDecision,
|
|
||||||
decided_by: str = "external",
|
|
||||||
reason: str | None = None,
|
|
||||||
) -> bool:
|
|
||||||
existing = self._results.get(request_id)
|
|
||||||
if existing is not None:
|
|
||||||
logger.warning(
|
|
||||||
"审批请求已达终态,无法重新决策: req=%s, current=%s, attempted=%s",
|
|
||||||
request_id,
|
|
||||||
existing.decision.value,
|
|
||||||
decision.value,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
request = self._pending.get(request_id)
|
|
||||||
if request is None:
|
|
||||||
return False
|
|
||||||
if not request.approver_ids:
|
|
||||||
logger.warning(
|
|
||||||
"审批身份校验失败: req=%s, 审批人列表为空",
|
|
||||||
request_id,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
if decided_by not in request.approver_ids:
|
|
||||||
logger.warning(
|
|
||||||
"审批身份校验失败: req=%s, decided_by=%s 不在审批人列表 %s 中",
|
|
||||||
request_id,
|
|
||||||
decided_by,
|
|
||||||
request.approver_ids,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
del self._pending[request_id]
|
|
||||||
result = ApprovalResult(
|
|
||||||
request_id=request_id,
|
|
||||||
decision=decision,
|
|
||||||
decided_by=decided_by,
|
|
||||||
reason=reason,
|
|
||||||
)
|
|
||||||
self._results[request_id] = result
|
|
||||||
self._signal(request_id)
|
|
||||||
logger.info(
|
|
||||||
"审批决策已注册: req=%s, decision=%s, decided_by=%s, reason=%s",
|
|
||||||
request_id,
|
|
||||||
decision.value,
|
|
||||||
decided_by,
|
|
||||||
reason,
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def cleanup_expired(self, max_age_seconds: float = 300) -> int:
|
|
||||||
removed = 0
|
|
||||||
now = time.monotonic()
|
|
||||||
stale = [
|
|
||||||
rid for rid, req in self._pending.items()
|
|
||||||
if now - req.created_at > max_age_seconds and rid not in self._results
|
|
||||||
]
|
|
||||||
for rid in stale:
|
|
||||||
del self._pending[rid]
|
|
||||||
result = ApprovalResult(
|
|
||||||
request_id=rid,
|
|
||||||
decision=ApprovalDecision.EXPIRED,
|
|
||||||
reason="清理过期审批",
|
|
||||||
)
|
|
||||||
self._results[rid] = result
|
|
||||||
self._signal(rid)
|
|
||||||
removed += 1
|
|
||||||
if removed > 0:
|
|
||||||
logger.info("清理过期审批请求: count=%d", removed)
|
|
||||||
return removed
|
|
||||||
|
|
||||||
def cleanup_results(self, max_age_seconds: float = 600) -> int:
|
|
||||||
removed = 0
|
|
||||||
now = time.monotonic()
|
|
||||||
stale = [rid for rid, res in self._results.items() if now - res.decided_at > max_age_seconds]
|
|
||||||
for rid in stale:
|
|
||||||
del self._results[rid]
|
|
||||||
removed += 1
|
|
||||||
if removed > 0:
|
|
||||||
logger.info("清理过期审批结果: count=%d", removed)
|
|
||||||
return removed
|
|
||||||
|
|
||||||
def start_cleanup_task(self, interval: float = 60) -> None:
|
|
||||||
if self._cleanup_task is not None and not self._cleanup_task.done():
|
|
||||||
return
|
|
||||||
self._cleanup_task = asyncio.create_task(self._run_cleanup_loop(interval))
|
|
||||||
|
|
||||||
def stop_cleanup_task(self) -> None:
|
|
||||||
if self._cleanup_task is not None and not self._cleanup_task.done():
|
|
||||||
self._cleanup_task.cancel()
|
|
||||||
|
|
||||||
async def _run_cleanup_loop(self, interval: float) -> None:
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
self.cleanup_expired()
|
|
||||||
self.cleanup_results()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _signal(self, request_id: str) -> None:
|
|
||||||
event = self._events.pop(request_id, None)
|
|
||||||
if event is not None:
|
|
||||||
event.set()
|
|
||||||
|
|
||||||
async def request_approval(
|
|
||||||
self,
|
|
||||||
channel_type: str,
|
|
||||||
account_id: str,
|
|
||||||
config: dict,
|
|
||||||
action: ApprovalAction,
|
|
||||||
initiator_peer_id: str,
|
|
||||||
description: str,
|
|
||||||
context: dict,
|
|
||||||
) -> ApprovalRequest | None:
|
|
||||||
plugin = ChannelPluginRegistry.get(channel_type)
|
|
||||||
if plugin is None or not isinstance(plugin, ApprovalProtocol):
|
|
||||||
return None
|
|
||||||
|
|
||||||
need = await plugin.check_approval_required(config, action, initiator_peer_id)
|
|
||||||
if not need:
|
|
||||||
return None
|
|
||||||
|
|
||||||
request = await plugin.create_approval_request(
|
|
||||||
config,
|
|
||||||
action,
|
|
||||||
initiator_peer_id,
|
|
||||||
description,
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
approvers = plugin.get_approver_ids(config)
|
|
||||||
if not approvers:
|
|
||||||
logger.warning(
|
|
||||||
"审批请求创建失败: channel=%s, 审批人列表为空",
|
|
||||||
channel_type,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
request.approver_ids = list(approvers)
|
|
||||||
sent = await plugin.send_approval_notification(config, request, approvers)
|
|
||||||
if not sent:
|
|
||||||
logger.warning(
|
|
||||||
"审批通知发送失败: req=%s, channel=%s, approvers=%s",
|
|
||||||
request.id,
|
|
||||||
channel_type,
|
|
||||||
approvers,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._pending[request.id] = request
|
|
||||||
return request
|
|
||||||
|
|
||||||
async def wait_for_decision(
|
|
||||||
self, channel_type: str, config: dict, request_id: str, timeout: float = 300
|
|
||||||
) -> ApprovalResult:
|
|
||||||
existing = self._results.get(request_id)
|
|
||||||
if existing is not None:
|
|
||||||
return existing
|
|
||||||
|
|
||||||
event = self._events.get(request_id)
|
|
||||||
if event is None:
|
|
||||||
event = asyncio.Event()
|
|
||||||
self._events[request_id] = event
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
deadline = loop.time() + timeout
|
|
||||||
while True:
|
|
||||||
remaining = deadline - loop.time()
|
|
||||||
if remaining <= 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
plugin_result = await self._check_and_update(channel_type, config, request_id)
|
|
||||||
if plugin_result is not None:
|
|
||||||
self._pending.pop(request_id, None)
|
|
||||||
self._results[request_id] = plugin_result
|
|
||||||
self._events.pop(request_id, None)
|
|
||||||
return plugin_result
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(event.wait(), timeout=min(remaining, 5))
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
stored = self._results.get(request_id)
|
|
||||||
if stored is not None:
|
|
||||||
self._events.pop(request_id, None)
|
|
||||||
return stored
|
|
||||||
|
|
||||||
self._events.pop(request_id, None)
|
|
||||||
expired = ApprovalResult(
|
|
||||||
request_id=request_id,
|
|
||||||
decision=ApprovalDecision.EXPIRED,
|
|
||||||
reason="审批超时",
|
|
||||||
)
|
|
||||||
self._pending.pop(request_id, None)
|
|
||||||
self._results[request_id] = expired
|
|
||||||
return expired
|
|
||||||
|
|
||||||
async def _check_and_update(self, channel_type: str, config: dict, request_id: str) -> ApprovalResult | None:
|
|
||||||
plugin = ChannelPluginRegistry.get(channel_type)
|
|
||||||
if plugin is None or not isinstance(plugin, ApprovalProtocol):
|
|
||||||
return None
|
|
||||||
return await plugin.check_approval_status(config, request_id)
|
|
||||||
Loading…
Reference in New Issue
Block a user