1. 抽离alt文本截断逻辑为公共工具函数 2. 重构快速回复构建函数,支持更多action类型 3. 新增 narrowcast 消息发送与进度查询能力 4. 新增用户粉丝数、好友人口统计API调用 5. 优化回复消息逻辑,支持quote token 6. 改进审批ID生成逻辑,避免重复 7. 调整部分配置与异常处理逻辑 8. 新增熔断器与相关指标统计
266 lines
8.4 KiB
Python
266 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import itertools
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
class LINEApprovalAdapter:
|
|
_id_counter = itertools.count()
|
|
|
|
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)}_{next(self._id_counter)}_{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)
|