ForcePilot/backend/package/yuxi/channels/adapters/line/approval.py
Kris a4ec94ef9d feat(line): 实现完整的 LINE 聊天适配器功能
新增 LINE 官方账号对接的全套功能,包括:
1. 基础的 Bot 探测、会话解析、消息格式化能力
2. 富媒体消息模板、快速回复、卡片指令支持
3. Webhook 签名验证、重放防护、多账户路由管理
4. 消息发送、回复、分块传输、用户绑定管理
5. 交互式配置向导与诊断工具
2026-05-12 00:45:33 +08:00

262 lines
8.4 KiB
Python

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)