Files
AstrBot/astrbot/dashboard/routes/platform.py
2026-05-19 21:15:22 +08:00

294 lines
11 KiB
Python

"""统一 Webhook 路由
提供统一的 webhook 回调入口,支持多个平台使用同一端口接收回调。
"""
import secrets
import string
from quart import request
from astrbot.core import logger
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
from astrbot.core.platform import Platform
from astrbot.core.platform.sources.dingtalk.app_registration import (
poll_dingtalk_app_registration_once,
request_dingtalk_app_registration,
)
from astrbot.core.platform.sources.lark.app_registration import (
poll_app_registration_once,
request_app_registration,
)
from astrbot.core.platform.sources.lark.bot_info import request_lark_bot_info
from astrbot.core.platform.sources.weixin_oc.login_registration import (
poll_weixin_oc_login_once,
request_weixin_oc_login_qr,
)
from .route import Response, Route, RouteContext
def _random_platform_id_suffix() -> str:
return "_" + "".join(secrets.choice(string.ascii_lowercase) for _ in range(4))
class PlatformRoute(Route):
"""统一 Webhook 路由"""
def __init__(
self,
context: RouteContext,
core_lifecycle: AstrBotCoreLifecycle,
) -> None:
super().__init__(context)
self.core_lifecycle = core_lifecycle
self.platform_manager = core_lifecycle.platform_manager
self._register_webhook_routes()
def _register_webhook_routes(self) -> None:
"""注册 webhook 路由"""
# 统一 webhook 入口,支持 GET 和 POST
self.app.add_url_rule(
"/api/platform/webhook/<webhook_uuid>",
view_func=self.unified_webhook_callback,
methods=["GET", "POST"],
)
# 平台统计信息接口
self.app.add_url_rule(
"/api/platform/stats",
view_func=self.get_platform_stats,
methods=["GET"],
)
self.app.add_url_rule(
"/api/platform/registration/<platform_type>",
view_func=self.handle_platform_registration,
methods=["POST"],
)
async def unified_webhook_callback(self, webhook_uuid: str):
"""统一 webhook 回调入口
Args:
webhook_uuid: 平台配置中的 webhook_uuid
Returns:
根据平台适配器返回相应的响应
"""
# 根据 webhook_uuid 查找对应的平台
platform_adapter = self._find_platform_by_uuid(webhook_uuid)
if not platform_adapter:
logger.warning(f"未找到 webhook_uuid 为 {webhook_uuid} 的平台")
return Response().error("未找到对应平台").to_json(), 404
# 调用平台适配器的 webhook_callback 方法
try:
result = await platform_adapter.webhook_callback(request)
return result
except NotImplementedError:
logger.error(
f"平台 {platform_adapter.meta().name} 未实现 webhook_callback 方法",
)
return Response().error("平台未支持统一 Webhook 模式").to_json(), 500
except Exception as e:
logger.error(f"处理 webhook 回调时发生错误: {e}", exc_info=True)
return Response().error("处理回调失败").to_json(), 500
def _find_platform_by_uuid(self, webhook_uuid: str) -> Platform | None:
"""根据 webhook_uuid 查找对应的平台适配器
Args:
webhook_uuid: webhook UUID
Returns:
平台适配器实例,未找到则返回 None
"""
if self.platform_manager is None:
return None
for platform in self.platform_manager.platform_insts:
if platform.config.get("webhook_uuid") == webhook_uuid:
if platform.unified_webhook():
return platform
return None
async def get_platform_stats(self):
"""获取所有平台的统计信息
Returns:
包含平台统计信息的响应
"""
try:
mgr = self.platform_manager
if mgr is None:
return Response().error("Platform manager not available").to_json()
stats = mgr.get_all_stats()
return Response().ok(stats).to_json()
except Exception as e:
logger.error(f"获取平台统计信息失败: {e}", exc_info=True)
return Response().error(f"获取统计信息失败: {e}").__dict__, 500
async def handle_platform_registration(self, platform_type: str):
"""Handle dashboard one-click platform registration actions."""
try:
payload = await request.get_json(silent=True) or {}
action = str(payload.get("action", "")).strip().lower()
if not action:
return Response().error("Missing action").__dict__, 400
platform_config = payload.get("platform_config")
if not isinstance(platform_config, dict):
platform_config = {}
if platform_type == "lark":
return await self._handle_lark_registration(
action,
payload,
platform_config,
)
if platform_type == "weixin_oc":
return await self._handle_weixin_oc_registration(
action,
payload,
platform_config,
)
if platform_type == "dingtalk":
return await self._handle_dingtalk_registration(action, payload)
return Response().error(
f"Unsupported platform registration: {platform_type}",
).__dict__, 404
except Exception as e:
logger.error(f"处理平台一键创建请求失败: {e}", exc_info=True)
return Response().error(str(e)).__dict__, 500
async def _handle_lark_registration(
self,
action: str,
payload: dict,
platform_config: dict,
):
domain = str(platform_config.get("domain") or "").strip()
if action == "start":
registration = await request_app_registration(domain)
return (
Response()
.ok(
{
"status": "pending",
"device_code": registration.device_code,
"registration_code": registration.device_code,
"user_code": registration.user_code,
"verification_uri": registration.verification_uri,
"verification_uri_complete": registration.verification_uri_complete,
"expires_in": registration.expires_in,
"interval": registration.interval,
},
)
.__dict__
)
if action == "poll":
device_code = str(
payload.get("device_code") or payload.get("registration_code") or "",
).strip()
if not device_code:
return Response().error("Missing device_code").__dict__, 400
result = await poll_app_registration_once(
domain=domain,
device_code=device_code,
)
if result.get("status") == "created":
try:
bot_info = await request_lark_bot_info(
domain=str(result.get("domain") or domain),
app_id=str(result.get("app_id") or ""),
app_secret=str(result.get("app_secret") or ""),
)
if bot_info.app_name:
result["bot_name"] = bot_info.app_name
if bot_info.open_id:
result["bot_open_id"] = bot_info.open_id
except Exception as e:
logger.error(f"获取飞书机器人信息失败: {e}", exc_info=True)
return Response().ok(result).__dict__
return Response().error(f"Unsupported action: {action}").__dict__, 400
async def _handle_dingtalk_registration(self, action: str, payload: dict):
if action == "start":
registration = await request_dingtalk_app_registration()
return (
Response()
.ok(
{
"status": "pending",
"device_code": registration.device_code,
"registration_code": registration.device_code,
"user_code": registration.user_code,
"verification_uri": registration.verification_uri,
"verification_uri_complete": registration.verification_uri_complete,
"expires_in": registration.expires_in,
"interval": registration.interval,
},
)
.__dict__
)
if action == "poll":
device_code = str(
payload.get("device_code") or payload.get("registration_code") or "",
).strip()
if not device_code:
return Response().error("Missing device_code").__dict__, 400
result = await poll_dingtalk_app_registration_once(device_code)
if result.get("status") == "created":
result["platform_id_suffix"] = _random_platform_id_suffix()
return Response().ok(result).__dict__
return Response().error(f"Unsupported action: {action}").__dict__, 400
async def _handle_weixin_oc_registration(
self,
action: str,
payload: dict,
platform_config: dict,
):
if action == "start":
registration = await request_weixin_oc_login_qr(platform_config)
return (
Response()
.ok(
{
"status": "pending",
"registration_code": registration.qrcode,
"qrcode": registration.qrcode,
"qrcode_img_content": registration.qrcode_img_content,
"interval": registration.interval,
},
)
.__dict__
)
if action == "poll":
qrcode = str(
payload.get("qrcode") or payload.get("registration_code") or "",
).strip()
if not qrcode:
return Response().error("Missing qrcode").__dict__, 400
result = await poll_weixin_oc_login_once(
platform_config=platform_config,
qrcode=qrcode,
)
if result.get("status") == "created":
result["platform_id_suffix"] = _random_platform_id_suffix()
return Response().ok(result).__dict__
return Response().error(f"Unsupported action: {action}").__dict__, 400