from __future__ import annotations from yuxi.utils.logging_config import logger class LINEApprovalAdapter: def __init__(self, adapter): self._adapter = adapter self._pending_approvals: dict[str, dict] = {} async def create_approval( self, chat_id: str, title: str, description: str, actions: list[dict] | None = None, timeout_seconds: int = 3600, scenario: str = "general", ) -> dict | None: import time approval_id = f"apr_{int(time.time() * 1000)}_{chat_id[:10]}" if actions is None: actions = [ {"label": "批准", "data": f"approval:{approval_id}:approved", "style": "primary"}, {"label": "拒绝", "data": f"approval:{approval_id}:rejected", "style": "danger"}, ] self._pending_approvals[approval_id] = { "chat_id": chat_id, "title": title, "description": description, "status": "pending", "scenario": scenario, "actions": actions, "created_at": time.time(), "expires_at": time.time() + timeout_seconds, "approver": None, "result": None, } body_contents: list[dict] = [ { "type": "text", "text": title, "weight": "bold", "size": "md", "wrap": True, }, { "type": "separator", "margin": "md", }, { "type": "text", "text": description[:400], "size": "sm", "wrap": True, "margin": "md", "color": "#555555", }, { "type": "text", "text": f"⏰ {timeout_seconds // 60} 分钟内有效", "size": "xs", "color": "#aaaaaa", "margin": "md", }, ] action_buttons = [] for action in actions[:4]: style = action.get("style", "primary") action_buttons.append( { "type": "button", "action": { "type": "postback", "label": action["label"][:20], "data": action["data"], "displayText": f"{action['label']}: {title[:30]}", }, "style": style, } ) bubble = { "type": "bubble", "header": { "type": "box", "layout": "vertical", "contents": [ { "type": "text", "text": "🔐 审批请求", "weight": "bold", "size": "lg", "color": "#ffffff", } ], "backgroundColor": "#FF6B6B", }, "body": { "type": "box", "layout": "vertical", "contents": body_contents, }, "footer": { "type": "box", "layout": "vertical", "contents": action_buttons, }, } message = { "type": "flex", "altText": f"审批: {title[:40]}", "contents": bubble, } result = await self._adapter._sender.push_message(chat_id, [message]) if result.success: logger.info(f"[LINE Approval] Created {scenario} approval {approval_id} for {chat_id}") return { "approval_id": approval_id, "status": "pending", "title": title, } del self._pending_approvals[approval_id] return None async def create_command_approval( self, chat_id: str, command: str, description: str, timeout_seconds: int = 600 ) -> dict | None: return await self.create_approval( chat_id=chat_id, title=f"命令执行: {command[:40]}", description=description, timeout_seconds=timeout_seconds, scenario="command_execution", ) async def create_config_approval( self, chat_id: str, config_key: str, old_value: str, new_value: str, timeout_seconds: int = 1200 ) -> dict | None: return await self.create_approval( chat_id=chat_id, title=f"配置变更: {config_key}", description=f"将 {config_key} 从 '{old_value}' 修改为 '{new_value}'", timeout_seconds=timeout_seconds, scenario="config_change", ) async def create_sensitive_approval( self, chat_id: str, operation: str, description: str, timeout_seconds: int = 900 ) -> dict | None: return await self.create_approval( chat_id=chat_id, title=f"敏感操作: {operation[:40]}", description=description, timeout_seconds=timeout_seconds, scenario="sensitive_operation", ) def resolve_approval_from_postback(self, postback_data: str) -> str | None: if not postback_data.startswith("approval:"): return None parts = postback_data.split(":") if len(parts) < 3: return None return parts[1] def resolve_approval_action(self, postback_data: str) -> str | None: if not postback_data.startswith("approval:"): return None parts = postback_data.split(":") if len(parts) < 3: return None return parts[2] def handle_approval_response(self, approval_id: str, action: str, approver_id: str) -> dict | None: approval = self._pending_approvals.get(approval_id) if not approval: return None import time if approval["expires_at"] and time.time() > approval["expires_at"]: approval["status"] = "expired" return {"approval_id": approval_id, "status": "expired"} if approval["status"] != "pending": return None approval["status"] = action approval["approver"] = approver_id approval["result"] = action approval["resolved_at"] = time.time() logger.info(f"[LINE Approval] {approval_id} {action} by {approver_id}") return { "approval_id": approval_id, "status": action, "approver": approver_id, "title": approval["title"], "scenario": approval.get("scenario", "general"), } def get_approval(self, approval_id: str) -> dict | None: approval = self._pending_approvals.get(approval_id) if not approval: return None return dict(approval) def list_pending_approvals(self, chat_id: str | None = None) -> list[dict]: import time results = [] for apr_id, approval in self._pending_approvals.items(): if chat_id and approval["chat_id"] != chat_id: continue if approval["expires_at"] and time.time() > approval["expires_at"]: approval["status"] = "expired" if approval["status"] == "pending": results.append(dict(approval)) return results def cancel_approval(self, approval_id: str) -> bool: approval = self._pending_approvals.get(approval_id) if not approval or approval["status"] != "pending": return False import time approval["status"] = "cancelled" approval["resolved_at"] = time.time() return True def cleanup_expired(self) -> int: import time now = time.time() expired_ids = [ apr_id for apr_id, approval in self._pending_approvals.items() if approval["expires_at"] and now > approval["expires_at"] ] for apr_id in expired_ids: self._pending_approvals[apr_id]["status"] = "expired" stale_ids = [ apr_id for apr_id, approval in self._pending_approvals.items() if approval.get("resolved_at") and now - approval["resolved_at"] > 86400 ] for apr_id in stale_ids: del self._pending_approvals[apr_id] return len(expired_ids) + len(stale_ids)