feat: unified platform webhook url (#3889)
* feat: unified platform webhook url * chore: ruff format * fix: 修复 Telegram 语音使用 Whisper API 报错 (#3884) * Update whisper_api_source.py * chore: ruff format --------- Co-authored-by: Soulter <905617992@qq.com> * Update astrbot/dashboard/routes/platform.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: ruff format * fix: update webhook dialog descriptions for clarity in English and Chinese locales * fix: update webhook URL paths to include '/api' prefix for consistency across the application --------- Co-authored-by: 易推倒白毛 <zhaixingbi@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,14 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
VERSION = "4.7.4"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
"qq_official_webhook",
|
||||
"weixin_official_account",
|
||||
"wecom",
|
||||
"wecom_ai_bot",
|
||||
"slack",
|
||||
]
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_CONFIG = {
|
||||
"config_version": 2,
|
||||
@@ -185,6 +193,8 @@ CONFIG_METADATA_2 = {
|
||||
"appid": "",
|
||||
"secret": "",
|
||||
"is_sandbox": False,
|
||||
"unified_webhook_mode": True,
|
||||
"webhook_uuid": "",
|
||||
"callback_server_host": "0.0.0.0",
|
||||
"port": 6196,
|
||||
},
|
||||
@@ -215,6 +225,8 @@ CONFIG_METADATA_2 = {
|
||||
"token": "",
|
||||
"encoding_aes_key": "",
|
||||
"api_base_url": "https://api.weixin.qq.com/cgi-bin/",
|
||||
"unified_webhook_mode": True,
|
||||
"webhook_uuid": "",
|
||||
"callback_server_host": "0.0.0.0",
|
||||
"port": 6194,
|
||||
"active_send_mode": False,
|
||||
@@ -229,6 +241,8 @@ CONFIG_METADATA_2 = {
|
||||
"encoding_aes_key": "",
|
||||
"kf_name": "",
|
||||
"api_base_url": "https://qyapi.weixin.qq.com/cgi-bin/",
|
||||
"unified_webhook_mode": True,
|
||||
"webhook_uuid": "",
|
||||
"callback_server_host": "0.0.0.0",
|
||||
"port": 6195,
|
||||
},
|
||||
@@ -241,6 +255,8 @@ CONFIG_METADATA_2 = {
|
||||
"wecom_ai_bot_name": "",
|
||||
"token": "",
|
||||
"encoding_aes_key": "",
|
||||
"unified_webhook_mode": True,
|
||||
"webhook_uuid": "",
|
||||
"callback_server_host": "0.0.0.0",
|
||||
"port": 6198,
|
||||
},
|
||||
@@ -308,6 +324,8 @@ CONFIG_METADATA_2 = {
|
||||
"app_token": "",
|
||||
"signing_secret": "",
|
||||
"slack_connection_mode": "socket", # webhook, socket
|
||||
"unified_webhook_mode": True,
|
||||
"webhook_uuid": "",
|
||||
"slack_webhook_host": "0.0.0.0",
|
||||
"slack_webhook_port": 6197,
|
||||
"slack_webhook_path": "/astrbot-slack-webhook/callback",
|
||||
@@ -387,16 +405,28 @@ CONFIG_METADATA_2 = {
|
||||
"description": "Slack Webhook Host",
|
||||
"type": "string",
|
||||
"hint": "Only valid when Slack connection mode is `webhook`.",
|
||||
"condition": {
|
||||
"slack_connection_mode": "webhook",
|
||||
"unified_webhook_mode": False,
|
||||
},
|
||||
},
|
||||
"slack_webhook_port": {
|
||||
"description": "Slack Webhook Port",
|
||||
"type": "int",
|
||||
"hint": "Only valid when Slack connection mode is `webhook`.",
|
||||
"condition": {
|
||||
"slack_connection_mode": "webhook",
|
||||
"unified_webhook_mode": False,
|
||||
},
|
||||
},
|
||||
"slack_webhook_path": {
|
||||
"description": "Slack Webhook Path",
|
||||
"type": "string",
|
||||
"hint": "Only valid when Slack connection mode is `webhook`.",
|
||||
"condition": {
|
||||
"slack_connection_mode": "webhook",
|
||||
"unified_webhook_mode": False,
|
||||
},
|
||||
},
|
||||
"active_send_mode": {
|
||||
"description": "是否换用主动发送接口",
|
||||
@@ -587,6 +617,33 @@ CONFIG_METADATA_2 = {
|
||||
"type": "string",
|
||||
"hint": "可选的 Discord 活动名称。留空则不设置活动。",
|
||||
},
|
||||
"port": {
|
||||
"description": "回调服务器端口",
|
||||
"type": "int",
|
||||
"hint": "回调服务器端口。留空则不启用回调服务器。",
|
||||
"condition": {
|
||||
"unified_webhook_mode": False,
|
||||
},
|
||||
},
|
||||
"callback_server_host": {
|
||||
"description": "回调服务器主机",
|
||||
"type": "string",
|
||||
"hint": "回调服务器主机。留空则不启用回调服务器。",
|
||||
"condition": {
|
||||
"unified_webhook_mode": False,
|
||||
},
|
||||
},
|
||||
"unified_webhook_mode": {
|
||||
"description": "统一 Webhook 模式",
|
||||
"type": "bool",
|
||||
"hint": "启用后,将使用 AstrBot 统一 Webhook 入口,无需单独开启端口。回调地址为 /api/platform/webhook/{webhook_uuid}。",
|
||||
},
|
||||
"webhook_uuid": {
|
||||
"invisible": True,
|
||||
"description": "Webhook UUID",
|
||||
"type": "string",
|
||||
"hint": "统一 Webhook 模式下的唯一标识符,创建平台时自动生成。",
|
||||
},
|
||||
},
|
||||
},
|
||||
"platform_settings": {
|
||||
|
||||
@@ -13,8 +13,10 @@ from .platform_metadata import PlatformMetadata
|
||||
|
||||
|
||||
class Platform(abc.ABC):
|
||||
def __init__(self, event_queue: Queue):
|
||||
def __init__(self, config: dict, event_queue: Queue):
|
||||
super().__init__()
|
||||
# 平台配置
|
||||
self.config = config
|
||||
# 维护了消息平台的事件队列,EventBus 会从这里取出事件并处理。
|
||||
self._event_queue = event_queue
|
||||
self.client_self_id = uuid.uuid4().hex
|
||||
@@ -36,7 +38,7 @@ class Platform(abc.ABC):
|
||||
self,
|
||||
session: MessageSesion,
|
||||
message_chain: MessageChain,
|
||||
) -> Awaitable[Any]:
|
||||
):
|
||||
"""通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。
|
||||
|
||||
异步方法。
|
||||
@@ -49,3 +51,20 @@ class Platform(abc.ABC):
|
||||
|
||||
def get_client(self):
|
||||
"""获取平台的客户端对象。"""
|
||||
|
||||
async def webhook_callback(self, request: Any) -> Any:
|
||||
"""统一 Webhook 回调入口。
|
||||
|
||||
支持统一 Webhook 模式的平台需要实现此方法。
|
||||
当 Dashboard 收到 /api/platform/webhook/{uuid} 请求时,会调用此方法。
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
响应内容,格式取决于具体平台的要求
|
||||
|
||||
Raises:
|
||||
NotImplementedError: 平台未实现统一 Webhook 模式
|
||||
"""
|
||||
raise NotImplementedError(f"平台 {self.meta().name} 未实现统一 Webhook 模式")
|
||||
|
||||
@@ -38,9 +38,8 @@ class AiocqhttpAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
super().__init__(platform_config, event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
self.settings = platform_settings
|
||||
self.unique_session = platform_settings["unique_session"]
|
||||
self.host = platform_config["ws_reverse_host"]
|
||||
@@ -154,7 +153,9 @@ class AiocqhttpAdapter(Platform):
|
||||
"""OneBot V11 通知类事件"""
|
||||
abm = AstrBotMessage()
|
||||
abm.self_id = str(event.self_id)
|
||||
abm.sender = MessageMember(user_id=str(event.user_id), nickname=event.user_id)
|
||||
abm.sender = MessageMember(
|
||||
user_id=str(event.user_id), nickname=str(event.user_id)
|
||||
)
|
||||
abm.type = MessageType.OTHER_MESSAGE
|
||||
if event.get("group_id"):
|
||||
abm.group_id = str(event.group_id)
|
||||
|
||||
@@ -47,9 +47,7 @@ class DingtalkPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
|
||||
self.unique_session = platform_settings["unique_session"]
|
||||
|
||||
@@ -76,13 +74,13 @@ class DingtalkPlatformAdapter(Platform):
|
||||
)
|
||||
self.client_ = client # 用于 websockets 的 client
|
||||
|
||||
def _id_to_sid(self, dingtalk_id: str | None) -> str | None:
|
||||
def _id_to_sid(self, dingtalk_id: str | None) -> str:
|
||||
if not dingtalk_id:
|
||||
return dingtalk_id
|
||||
return dingtalk_id or "unknown"
|
||||
prefix = "$:LWCP_v1:$"
|
||||
if dingtalk_id.startswith(prefix):
|
||||
return dingtalk_id[len(prefix) :]
|
||||
return dingtalk_id
|
||||
return dingtalk_id or "unknown"
|
||||
|
||||
async def send_by_session(
|
||||
self,
|
||||
|
||||
@@ -44,8 +44,7 @@ class DiscordPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settings = platform_settings
|
||||
self.client_self_id = None
|
||||
self.registered_handlers = []
|
||||
|
||||
@@ -33,9 +33,7 @@ class LarkPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
|
||||
self.unique_session = platform_settings["unique_session"]
|
||||
|
||||
|
||||
@@ -55,8 +55,7 @@ class MisskeyPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config or {}
|
||||
super().__init__(platform_config or {}, event_queue)
|
||||
self.settings = platform_settings or {}
|
||||
self.instance_url = self.config.get("misskey_instance_url", "")
|
||||
self.access_token = self.config.get("misskey_token", "")
|
||||
|
||||
@@ -97,9 +97,7 @@ class QQOfficialPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
|
||||
self.appid = platform_config["appid"]
|
||||
self.secret = platform_config["secret"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import botpy
|
||||
import botpy.message
|
||||
@@ -11,6 +12,7 @@ from astrbot import logger
|
||||
from astrbot.api.event import MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, MessageType, Platform, PlatformMetadata
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from ..qqofficial.qqofficial_platform_adapter import QQOfficialPlatformAdapter
|
||||
@@ -87,13 +89,12 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
|
||||
self.appid = platform_config["appid"]
|
||||
self.secret = platform_config["secret"]
|
||||
self.unique_session = platform_settings["unique_session"]
|
||||
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
||||
|
||||
intents = botpy.Intents(
|
||||
public_messages=True,
|
||||
@@ -106,6 +107,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
||||
timeout=20,
|
||||
)
|
||||
self.client.set_platform(self)
|
||||
self.webhook_helper = None
|
||||
|
||||
async def send_by_session(
|
||||
self,
|
||||
@@ -128,16 +130,37 @@ class QQOfficialWebhookPlatformAdapter(Platform):
|
||||
self.client,
|
||||
)
|
||||
await self.webhook_helper.initialize()
|
||||
await self.webhook_helper.start_polling()
|
||||
|
||||
# 如果启用统一 webhook 模式,则不启动独立服务器
|
||||
webhook_uuid = self.config.get("webhook_uuid")
|
||||
if self.unified_webhook_mode and webhook_uuid:
|
||||
log_webhook_info(f"{self.meta().id}(QQ 官方机器人 Webhook)", webhook_uuid)
|
||||
# 保持运行状态,等待 shutdown
|
||||
await self.webhook_helper.shutdown_event.wait()
|
||||
else:
|
||||
await self.webhook_helper.start_polling()
|
||||
|
||||
def get_client(self) -> botClient:
|
||||
return self.client
|
||||
|
||||
async def webhook_callback(self, request: Any) -> Any:
|
||||
"""统一 Webhook 回调入口"""
|
||||
if not self.webhook_helper:
|
||||
return {"error": "Webhook helper not initialized"}, 500
|
||||
|
||||
# 复用 webhook_helper 的回调处理逻辑
|
||||
return await self.webhook_helper.handle_callback(request)
|
||||
|
||||
async def terminate(self):
|
||||
self.webhook_helper.shutdown_event.set()
|
||||
if self.webhook_helper:
|
||||
self.webhook_helper.shutdown_event.set()
|
||||
await self.client.close()
|
||||
try:
|
||||
await self.webhook_helper.server.shutdown()
|
||||
except Exception as _:
|
||||
pass
|
||||
if self.webhook_helper and not self.unified_webhook_mode:
|
||||
try:
|
||||
await self.webhook_helper.server.shutdown()
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
f"Exception occurred during QQOfficialWebhook server shutdown: {exc}",
|
||||
exc_info=True,
|
||||
)
|
||||
logger.info("QQ 机器人官方 API 适配器已经被优雅地关闭")
|
||||
|
||||
@@ -78,7 +78,19 @@ class QQOfficialWebhook:
|
||||
return response
|
||||
|
||||
async def callback(self):
|
||||
msg: dict = await quart.request.json
|
||||
"""内部服务器的回调入口"""
|
||||
return await self.handle_callback(quart.request)
|
||||
|
||||
async def handle_callback(self, request) -> dict:
|
||||
"""处理 webhook 回调,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
msg: dict = await request.json
|
||||
logger.debug(f"收到 qq_official_webhook 回调: {msg}")
|
||||
|
||||
event = msg.get("t")
|
||||
|
||||
@@ -38,8 +38,7 @@ class SatoriPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settings = platform_settings
|
||||
|
||||
self.api_base_url = self.config.get(
|
||||
|
||||
@@ -47,51 +47,62 @@ class SlackWebhookClient:
|
||||
|
||||
@self.app.route(self.path, methods=["POST"])
|
||||
async def slack_events():
|
||||
"""处理 Slack 事件"""
|
||||
try:
|
||||
# 获取请求体和头部
|
||||
body = await request.get_data()
|
||||
event_data = json.loads(body.decode("utf-8"))
|
||||
|
||||
# Verify Slack request signature
|
||||
timestamp = request.headers.get("X-Slack-Request-Timestamp")
|
||||
signature = request.headers.get("X-Slack-Signature")
|
||||
if not timestamp or not signature:
|
||||
return Response("Missing headers", status=400)
|
||||
# Calculate the HMAC signature
|
||||
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
||||
my_signature = (
|
||||
"v0="
|
||||
+ hmac.new(
|
||||
self.signing_secret.encode("utf-8"),
|
||||
sig_basestring.encode("utf-8"),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
)
|
||||
# Verify the signature
|
||||
if not hmac.compare_digest(my_signature, signature):
|
||||
logger.warning("Slack request signature verification failed")
|
||||
return Response("Invalid signature", status=400)
|
||||
logger.info(f"Received Slack event: {event_data}")
|
||||
|
||||
# 处理 URL 验证事件
|
||||
if event_data.get("type") == "url_verification":
|
||||
return {"challenge": event_data.get("challenge")}
|
||||
# 处理事件
|
||||
if self.event_handler and event_data.get("type") == "event_callback":
|
||||
await self.event_handler(event_data)
|
||||
|
||||
return Response("", status=200)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理 Slack 事件时出错: {e}")
|
||||
return Response("Internal Server Error", status=500)
|
||||
"""内部服务器的 POST 回调入口"""
|
||||
return await self.handle_callback(request)
|
||||
|
||||
@self.app.route("/health", methods=["GET"])
|
||||
async def health_check():
|
||||
"""健康检查端点"""
|
||||
return {"status": "ok", "service": "slack-webhook"}
|
||||
|
||||
async def handle_callback(self, req):
|
||||
"""处理 Slack 回调请求,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
req: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
Response 对象或字典
|
||||
"""
|
||||
try:
|
||||
# 获取请求体和头部
|
||||
body = await req.get_data()
|
||||
event_data = json.loads(body.decode("utf-8"))
|
||||
|
||||
# Verify Slack request signature
|
||||
timestamp = req.headers.get("X-Slack-Request-Timestamp")
|
||||
signature = req.headers.get("X-Slack-Signature")
|
||||
if not timestamp or not signature:
|
||||
return Response("Missing headers", status=400)
|
||||
# Calculate the HMAC signature
|
||||
sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
|
||||
my_signature = (
|
||||
"v0="
|
||||
+ hmac.new(
|
||||
self.signing_secret.encode("utf-8"),
|
||||
sig_basestring.encode("utf-8"),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
)
|
||||
# Verify the signature
|
||||
if not hmac.compare_digest(my_signature, signature):
|
||||
logger.warning("Slack request signature verification failed")
|
||||
return Response("Invalid signature", status=400)
|
||||
logger.info(f"Received Slack event: {event_data}")
|
||||
|
||||
# 处理 URL 验证事件
|
||||
if event_data.get("type") == "url_verification":
|
||||
return {"challenge": event_data.get("challenge")}
|
||||
# 处理事件
|
||||
if self.event_handler and event_data.get("type") == "event_callback":
|
||||
await self.event_handler(event_data)
|
||||
|
||||
return Response("", status=200)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理 Slack 事件时出错: {e}")
|
||||
return Response("Internal Server Error", status=500)
|
||||
|
||||
async def start(self):
|
||||
"""启动 Webhook 服务器"""
|
||||
logger.info(
|
||||
|
||||
@@ -21,6 +21,7 @@ from astrbot.api.platform import (
|
||||
PlatformMetadata,
|
||||
)
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from .client import SlackSocketClient, SlackWebhookClient
|
||||
@@ -39,9 +40,7 @@ class SlackAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settings = platform_settings
|
||||
self.unique_session = platform_settings.get("unique_session", False)
|
||||
|
||||
@@ -49,6 +48,7 @@ class SlackAdapter(Platform):
|
||||
self.app_token = platform_config.get("app_token")
|
||||
self.signing_secret = platform_config.get("signing_secret")
|
||||
self.connection_mode = platform_config.get("slack_connection_mode", "socket")
|
||||
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
||||
self.webhook_host = platform_config.get("slack_webhook_host", "0.0.0.0")
|
||||
self.webhook_port = platform_config.get("slack_webhook_port", 3000)
|
||||
self.webhook_path = platform_config.get(
|
||||
@@ -361,10 +361,17 @@ class SlackAdapter(Platform):
|
||||
self._handle_webhook_event,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
|
||||
)
|
||||
await self.webhook_client.start()
|
||||
# 如果启用统一 webhook 模式,则不启动独立服务器
|
||||
webhook_uuid = self.config.get("webhook_uuid")
|
||||
if self.unified_webhook_mode and webhook_uuid:
|
||||
log_webhook_info(f"{self.meta().id}(Slack)", webhook_uuid)
|
||||
# 保持运行状态,等待 shutdown
|
||||
await self.webhook_client.shutdown_event.wait()
|
||||
else:
|
||||
logger.info(
|
||||
f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...",
|
||||
)
|
||||
await self.webhook_client.start()
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
@@ -391,6 +398,13 @@ class SlackAdapter(Platform):
|
||||
if abm:
|
||||
await self.handle_msg(abm)
|
||||
|
||||
async def webhook_callback(self, request: Any) -> Any:
|
||||
"""统一 Webhook 回调入口"""
|
||||
if self.connection_mode != "webhook" or not self.webhook_client:
|
||||
return {"error": "Slack adapter is not in webhook mode"}, 400
|
||||
|
||||
return await self.webhook_client.handle_callback(request)
|
||||
|
||||
async def terminate(self):
|
||||
if self.socket_client:
|
||||
await self.socket_client.stop()
|
||||
|
||||
@@ -42,8 +42,7 @@ class TelegramPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settings = platform_settings
|
||||
self.client_self_id = uuid.uuid4().hex[:8]
|
||||
|
||||
|
||||
@@ -76,9 +76,8 @@ class WebChatAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
super().__init__(platform_config, event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
self.settings = platform_settings
|
||||
self.unique_session = platform_settings["unique_session"]
|
||||
self.imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs")
|
||||
|
||||
@@ -42,10 +42,9 @@ class WeChatPadProAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
super().__init__(platform_config, event_queue)
|
||||
self._shutdown_event = None
|
||||
self.wxnewpass = None
|
||||
self.config = platform_config
|
||||
self.settings = platform_settings
|
||||
self.unique_session = platform_settings.get("unique_session", False)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import quart
|
||||
from requests import Response
|
||||
@@ -24,6 +25,7 @@ from astrbot.api.platform import (
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
from .wecom_event import WecomPlatformEvent
|
||||
from .wecom_kf import WeChatKF
|
||||
@@ -62,8 +64,20 @@ class WecomServer:
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
async def verify(self):
|
||||
logger.info(f"验证请求有效性: {quart.request.args}")
|
||||
args = quart.request.args
|
||||
"""内部服务器的 GET 验证入口"""
|
||||
return await self.handle_verify(quart.request)
|
||||
|
||||
async def handle_verify(self, request) -> str:
|
||||
"""处理验证请求,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
验证响应
|
||||
"""
|
||||
logger.info(f"验证请求有效性: {request.args}")
|
||||
args = request.args
|
||||
try:
|
||||
echo_str = self.crypto.check_signature(
|
||||
args.get("msg_signature"),
|
||||
@@ -78,10 +92,22 @@ class WecomServer:
|
||||
raise
|
||||
|
||||
async def callback_command(self):
|
||||
data = await quart.request.get_data()
|
||||
msg_signature = quart.request.args.get("msg_signature")
|
||||
timestamp = quart.request.args.get("timestamp")
|
||||
nonce = quart.request.args.get("nonce")
|
||||
"""内部服务器的 POST 回调入口"""
|
||||
return await self.handle_callback(quart.request)
|
||||
|
||||
async def handle_callback(self, request) -> str:
|
||||
"""处理回调请求,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
响应内容
|
||||
"""
|
||||
data = await request.get_data()
|
||||
msg_signature = request.args.get("msg_signature")
|
||||
timestamp = request.args.get("timestamp")
|
||||
nonce = request.args.get("nonce")
|
||||
try:
|
||||
xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
|
||||
except InvalidSignatureException:
|
||||
@@ -118,14 +144,14 @@ class WecomPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settingss = platform_settings
|
||||
self.client_self_id = uuid.uuid4().hex[:8]
|
||||
self.api_base_url = platform_config.get(
|
||||
"api_base_url",
|
||||
"https://qyapi.weixin.qq.com/cgi-bin/",
|
||||
)
|
||||
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
||||
|
||||
if not self.api_base_url:
|
||||
self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
|
||||
@@ -232,7 +258,23 @@ class WecomPlatformAdapter(Platform):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
await self.server.start_polling()
|
||||
|
||||
# 如果启用统一 webhook 模式,则不启动独立服务器
|
||||
webhook_uuid = self.config.get("webhook_uuid")
|
||||
if self.unified_webhook_mode and webhook_uuid:
|
||||
log_webhook_info(f"{self.meta().id}(企业微信)", webhook_uuid)
|
||||
# 保持运行状态,等待 shutdown
|
||||
await self.server.shutdown_event.wait()
|
||||
else:
|
||||
await self.server.start_polling()
|
||||
|
||||
async def webhook_callback(self, request: Any) -> Any:
|
||||
"""统一 Webhook 回调入口"""
|
||||
# 根据请求方法分发到不同的处理函数
|
||||
if request.method == "GET":
|
||||
return await self.server.handle_verify(request)
|
||||
else:
|
||||
return await self.server.handle_callback(request)
|
||||
|
||||
async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None:
|
||||
abm = AstrBotMessage()
|
||||
|
||||
@@ -22,6 +22,7 @@ from astrbot.api.platform import (
|
||||
PlatformMetadata,
|
||||
)
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
from ...register import register_platform_adapter
|
||||
from .wecomai_api import (
|
||||
@@ -103,9 +104,7 @@ class WecomAIBotAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settings = platform_settings
|
||||
|
||||
# 初始化配置参数
|
||||
@@ -122,6 +121,7 @@ class WecomAIBotAdapter(Platform):
|
||||
"wecomaibot_friend_message_welcome_text",
|
||||
"",
|
||||
)
|
||||
self.unified_webhook_mode = self.config.get("unified_webhook_mode", False)
|
||||
|
||||
# 平台元数据
|
||||
self.metadata = PlatformMetadata(
|
||||
@@ -425,17 +425,34 @@ class WecomAIBotAdapter(Platform):
|
||||
|
||||
def run(self) -> Awaitable[Any]:
|
||||
"""运行适配器,同时启动HTTP服务器和队列监听器"""
|
||||
logger.info("启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port)
|
||||
|
||||
async def run_both():
|
||||
# 同时运行HTTP服务器和队列监听器
|
||||
await asyncio.gather(
|
||||
self.server.start_server(),
|
||||
self.queue_listener.run(),
|
||||
)
|
||||
# 如果启用统一 webhook 模式,则不启动独立服务器
|
||||
webhook_uuid = self.config.get("webhook_uuid")
|
||||
if self.unified_webhook_mode and webhook_uuid:
|
||||
log_webhook_info(f"{self.meta().id}(企业微信智能机器人)", webhook_uuid)
|
||||
# 只运行队列监听器
|
||||
await self.queue_listener.run()
|
||||
else:
|
||||
logger.info(
|
||||
"启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port
|
||||
)
|
||||
# 同时运行HTTP服务器和队列监听器
|
||||
await asyncio.gather(
|
||||
self.server.start_server(),
|
||||
self.queue_listener.run(),
|
||||
)
|
||||
|
||||
return run_both()
|
||||
|
||||
async def webhook_callback(self, request: Any) -> Any:
|
||||
"""统一 Webhook 回调入口"""
|
||||
# 根据请求方法分发到不同的处理函数
|
||||
if request.method == "GET":
|
||||
return await self.server.handle_verify(request)
|
||||
else:
|
||||
return await self.server.handle_callback(request)
|
||||
|
||||
async def terminate(self):
|
||||
"""终止适配器"""
|
||||
logger.info("企业微信智能机器人适配器正在关闭...")
|
||||
|
||||
@@ -59,8 +59,19 @@ class WecomAIBotServer:
|
||||
)
|
||||
|
||||
async def verify_url(self):
|
||||
"""验证回调 URL"""
|
||||
args = quart.request.args
|
||||
"""内部服务器的 GET 验证入口"""
|
||||
return await self.handle_verify(quart.request)
|
||||
|
||||
async def handle_verify(self, request):
|
||||
"""处理 URL 验证请求,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
验证响应元组 (content, status_code, headers)
|
||||
"""
|
||||
args = request.args
|
||||
msg_signature = args.get("msg_signature")
|
||||
timestamp = args.get("timestamp")
|
||||
nonce = args.get("nonce")
|
||||
@@ -81,8 +92,19 @@ class WecomAIBotServer:
|
||||
return result, 200, {"Content-Type": "text/plain"}
|
||||
|
||||
async def handle_message(self):
|
||||
"""处理消息回调"""
|
||||
args = quart.request.args
|
||||
"""内部服务器的 POST 消息回调入口"""
|
||||
return await self.handle_callback(quart.request)
|
||||
|
||||
async def handle_callback(self, request):
|
||||
"""处理消息回调,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
响应元组 (content, status_code, headers)
|
||||
"""
|
||||
args = request.args
|
||||
msg_signature = args.get("msg_signature")
|
||||
timestamp = args.get("timestamp")
|
||||
nonce = args.get("nonce")
|
||||
@@ -102,7 +124,7 @@ class WecomAIBotServer:
|
||||
|
||||
try:
|
||||
# 获取请求体
|
||||
post_data = await quart.request.get_data()
|
||||
post_data = await request.get_data()
|
||||
|
||||
# 确保 post_data 是 bytes 类型
|
||||
if isinstance(post_data, str):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import quart
|
||||
from requests import Response
|
||||
@@ -22,6 +23,7 @@ from astrbot.api.platform import (
|
||||
)
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.utils.webhook_utils import log_webhook_info
|
||||
|
||||
from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
|
||||
|
||||
@@ -31,7 +33,7 @@ else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
class WecomServer:
|
||||
class WeixinOfficialAccountServer:
|
||||
def __init__(self, event_queue: asyncio.Queue, config: dict):
|
||||
self.server = quart.Quart(__name__)
|
||||
self.port = int(config.get("port"))
|
||||
@@ -57,9 +59,21 @@ class WecomServer:
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
async def verify(self):
|
||||
logger.info(f"验证请求有效性: {quart.request.args}")
|
||||
"""内部服务器的 GET 验证入口"""
|
||||
return await self.handle_verify(quart.request)
|
||||
|
||||
args = quart.request.args
|
||||
async def handle_verify(self, request) -> str:
|
||||
"""处理验证请求,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
验证响应
|
||||
"""
|
||||
logger.info(f"验证请求有效性: {request.args}")
|
||||
|
||||
args = request.args
|
||||
if not args.get("signature", None):
|
||||
logger.error("未知的响应,请检查回调地址是否填写正确。")
|
||||
return "err"
|
||||
@@ -77,10 +91,22 @@ class WecomServer:
|
||||
return "err"
|
||||
|
||||
async def callback_command(self):
|
||||
data = await quart.request.get_data()
|
||||
msg_signature = quart.request.args.get("msg_signature")
|
||||
timestamp = quart.request.args.get("timestamp")
|
||||
nonce = quart.request.args.get("nonce")
|
||||
"""内部服务器的 POST 回调入口"""
|
||||
return await self.handle_callback(quart.request)
|
||||
|
||||
async def handle_callback(self, request) -> str:
|
||||
"""处理回调请求,可被统一 webhook 入口复用
|
||||
|
||||
Args:
|
||||
request: Quart 请求对象
|
||||
|
||||
Returns:
|
||||
响应内容
|
||||
"""
|
||||
data = await request.get_data()
|
||||
msg_signature = request.args.get("msg_signature")
|
||||
timestamp = request.args.get("timestamp")
|
||||
nonce = request.args.get("nonce")
|
||||
try:
|
||||
xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
|
||||
except InvalidSignatureException:
|
||||
@@ -123,8 +149,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
||||
platform_settings: dict,
|
||||
event_queue: asyncio.Queue,
|
||||
) -> None:
|
||||
super().__init__(event_queue)
|
||||
self.config = platform_config
|
||||
super().__init__(platform_config, event_queue)
|
||||
self.settingss = platform_settings
|
||||
self.client_self_id = uuid.uuid4().hex[:8]
|
||||
self.api_base_url = platform_config.get(
|
||||
@@ -132,6 +157,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
||||
"https://api.weixin.qq.com/cgi-bin/",
|
||||
)
|
||||
self.active_send_mode = self.config.get("active_send_mode", False)
|
||||
self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
|
||||
|
||||
if not self.api_base_url:
|
||||
self.api_base_url = "https://api.weixin.qq.com/cgi-bin/"
|
||||
@@ -143,7 +169,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
||||
if not self.api_base_url.endswith("/"):
|
||||
self.api_base_url += "/"
|
||||
|
||||
self.server = WecomServer(self._event_queue, self.config)
|
||||
self.server = WeixinOfficialAccountServer(self._event_queue, self.config)
|
||||
|
||||
self.client = WeChatClient(
|
||||
self.config["appid"].strip(),
|
||||
@@ -202,7 +228,22 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
|
||||
|
||||
@override
|
||||
async def run(self):
|
||||
await self.server.start_polling()
|
||||
# 如果启用统一 webhook 模式,则不启动独立服务器
|
||||
webhook_uuid = self.config.get("webhook_uuid")
|
||||
if self.unified_webhook_mode and webhook_uuid:
|
||||
log_webhook_info(f"{self.meta().id}(微信公众平台)", webhook_uuid)
|
||||
# 保持运行状态,等待 shutdown
|
||||
await self.server.shutdown_event.wait()
|
||||
else:
|
||||
await self.server.start_polling()
|
||||
|
||||
async def webhook_callback(self, request: Any) -> Any:
|
||||
"""统一 Webhook 回调入口"""
|
||||
# 根据请求方法分发到不同的处理函数
|
||||
if request.method == "GET":
|
||||
return await self.server.handle_verify(request)
|
||||
else:
|
||||
return await self.server.handle_callback(request)
|
||||
|
||||
async def convert_message(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from astrbot.core import astrbot_config, logger
|
||||
|
||||
|
||||
def _get_callback_api_base() -> str:
|
||||
try:
|
||||
return astrbot_config.get("callback_api_base", "").rstrip("/")
|
||||
except Exception as e:
|
||||
logger.error(f"获取 callback_api_base 失败: {e!s}")
|
||||
return ""
|
||||
|
||||
|
||||
def _get_dashboard_port() -> int:
|
||||
try:
|
||||
return astrbot_config.get("dashboard", {}).get("port", 6185)
|
||||
except Exception as e:
|
||||
logger.error(f"获取 dashboard 端口失败: {e!s}")
|
||||
return 6185
|
||||
|
||||
|
||||
def log_webhook_info(platform_name: str, webhook_uuid: str):
|
||||
"""打印美观的 webhook 信息日志
|
||||
|
||||
Args:
|
||||
platform_name: 平台名称
|
||||
webhook_uuid: webhook 的 UUID
|
||||
"""
|
||||
|
||||
callback_base = _get_callback_api_base()
|
||||
|
||||
if not callback_base:
|
||||
callback_base = "http(s)://<your-astrbot-domain>"
|
||||
|
||||
if not callback_base.startswith("http"):
|
||||
callback_base = f"http(s)://{callback_base}"
|
||||
|
||||
callback_base = callback_base.rstrip("/")
|
||||
webhook_url = f"{callback_base}/api/platform/webhook/{webhook_uuid}"
|
||||
|
||||
display_log = (
|
||||
"\n====================\n"
|
||||
f"🔗 机器人平台 {platform_name} 已启用统一 Webhook 模式\n"
|
||||
f"📍 Webhook 回调地址: \n"
|
||||
f" ➜ http://<your-ip>:{_get_dashboard_port()}/api/platform/webhook/{webhook_uuid}\n"
|
||||
f" ➜ {webhook_url}\n"
|
||||
"====================\n"
|
||||
)
|
||||
logger.info(display_log)
|
||||
@@ -6,6 +6,7 @@ from .file import FileRoute
|
||||
from .knowledge_base import KnowledgeBaseRoute
|
||||
from .log import LogRoute
|
||||
from .persona import PersonaRoute
|
||||
from .platform import PlatformRoute
|
||||
from .plugin import PluginRoute
|
||||
from .session_management import SessionManagementRoute
|
||||
from .stat import StatRoute
|
||||
@@ -22,6 +23,7 @@ __all__ = [
|
||||
"KnowledgeBaseRoute",
|
||||
"LogRoute",
|
||||
"PersonaRoute",
|
||||
"PlatformRoute",
|
||||
"PluginRoute",
|
||||
"SessionManagementRoute",
|
||||
"StatRoute",
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import inspect
|
||||
import os
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
from quart import request
|
||||
|
||||
@@ -13,6 +14,7 @@ from astrbot.core.config.default import (
|
||||
CONFIG_METADATA_3_SYSTEM,
|
||||
DEFAULT_CONFIG,
|
||||
DEFAULT_VALUE_MAP,
|
||||
WEBHOOK_SUPPORTED_PLATFORMS,
|
||||
)
|
||||
from astrbot.core.config.i18n_utils import ConfigMetadataI18n
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
@@ -555,6 +557,15 @@ class ConfigRoute(Route):
|
||||
|
||||
async def post_new_platform(self):
|
||||
new_platform_config = await request.json
|
||||
|
||||
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,自动生成 webhook_uuid
|
||||
platform_type = new_platform_config.get("type", "")
|
||||
if platform_type in WEBHOOK_SUPPORTED_PLATFORMS:
|
||||
if new_platform_config.get("unified_webhook_mode", False):
|
||||
# 如果没有 webhook_uuid 或为空,自动生成
|
||||
if not new_platform_config.get("webhook_uuid"):
|
||||
new_platform_config["webhook_uuid"] = uuid.uuid4().hex[:16]
|
||||
|
||||
self.config["platform"].append(new_platform_config)
|
||||
try:
|
||||
save_config(self.config, self.config, is_core=True)
|
||||
@@ -584,6 +595,14 @@ class ConfigRoute(Route):
|
||||
if not platform_id or not new_config:
|
||||
return Response().error("参数错误").__dict__
|
||||
|
||||
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid
|
||||
platform_type = new_config.get("type", "")
|
||||
if platform_type in WEBHOOK_SUPPORTED_PLATFORMS:
|
||||
if new_config.get("unified_webhook_mode", False):
|
||||
# 如果没有 webhook_uuid 或为空,自动生成
|
||||
if not new_config.get("webhook_uuid"):
|
||||
new_config["webhook_uuid"] = uuid.uuid4().hex
|
||||
|
||||
for i, platform in enumerate(self.config["platform"]):
|
||||
if platform["id"] == platform_id:
|
||||
self.config["platform"][i] = new_config
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""统一 Webhook 路由
|
||||
|
||||
提供统一的 webhook 回调入口,支持多个平台使用同一端口接收回调。
|
||||
"""
|
||||
|
||||
from quart import request
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.platform import Platform
|
||||
|
||||
from .route import Response, Route, RouteContext
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 路由不使用标准的 /api 前缀,因为 webhook 回调需要直接访问
|
||||
# 所以我们手动注册路由
|
||||
self._register_webhook_routes()
|
||||
|
||||
def _register_webhook_routes(self):
|
||||
"""注册 webhook 路由"""
|
||||
# 统一 webhook 入口,支持 GET 和 POST
|
||||
self.app.add_url_rule(
|
||||
"/api/platform/webhook/<webhook_uuid>",
|
||||
view_func=self.unified_webhook_callback,
|
||||
methods=["GET", "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("未找到对应平台").__dict__, 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 模式").__dict__, 500
|
||||
except Exception as e:
|
||||
logger.error(f"处理 webhook 回调时发生错误: {e}", exc_info=True)
|
||||
return Response().error("处理回调失败").__dict__, 500
|
||||
|
||||
def _find_platform_by_uuid(self, webhook_uuid: str) -> Platform | None:
|
||||
"""根据 webhook_uuid 查找对应的平台适配器
|
||||
|
||||
Args:
|
||||
webhook_uuid: webhook UUID
|
||||
|
||||
Returns:
|
||||
平台适配器实例,未找到则返回 None
|
||||
"""
|
||||
for platform in self.platform_manager.platform_insts:
|
||||
if platform.config.get("webhook_uuid") == webhook_uuid:
|
||||
if platform.config.get("unified_webhook_mode", False):
|
||||
return platform
|
||||
return None
|
||||
@@ -16,6 +16,7 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.utils.io import get_local_ip_addresses
|
||||
|
||||
from .routes import *
|
||||
from .routes.platform import PlatformRoute
|
||||
from .routes.route import Response, RouteContext
|
||||
from .routes.session_management import SessionManagementRoute
|
||||
from .routes.t2i import T2iRoute
|
||||
@@ -79,6 +80,7 @@ class AstrBotDashboard:
|
||||
self.persona_route = PersonaRoute(self.context, db, core_lifecycle)
|
||||
self.t2i_route = T2iRoute(self.context, core_lifecycle)
|
||||
self.kb_route = KnowledgeBaseRoute(self.context, core_lifecycle)
|
||||
self.platform_route = PlatformRoute(self.context, core_lifecycle)
|
||||
|
||||
self.app.add_url_rule(
|
||||
"/api/plug/<path:subpath>",
|
||||
@@ -102,7 +104,7 @@ class AstrBotDashboard:
|
||||
async def auth_middleware(self):
|
||||
if not request.path.startswith("/api"):
|
||||
return None
|
||||
allowed_endpoints = ["/api/auth/login", "/api/file"]
|
||||
allowed_endpoints = ["/api/auth/login", "/api/file", "/api/platform/webhook"]
|
||||
if any(request.path.startswith(prefix) for prefix in allowed_endpoints):
|
||||
return None
|
||||
# 声明 JWT
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"adapters": "Platform Adapters",
|
||||
"addAdapter": "Add Adapter",
|
||||
"emptyText": "No platform adapters yet, click Add Adapter to create one",
|
||||
"viewWebhook": "View Webhook URL",
|
||||
"webhookCopied": "Webhook URL copied to clipboard",
|
||||
"webhookCopyFailed": "Copy failed, please copy manually",
|
||||
"webhookDialog": {
|
||||
"title": "Webhook Callback URL",
|
||||
"description": "The callback address is as follows, please ensure that the network environment can be accessed. You can also view the callback address information in the logs.",
|
||||
"close": "Close"
|
||||
},
|
||||
"details": {
|
||||
"adapterType": "Adapter Type",
|
||||
"token": "Token",
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"adapters": "平台适配器",
|
||||
"addAdapter": "创建机器人",
|
||||
"emptyText": "暂无平台适配器,点击 创建机器人 添加",
|
||||
"viewWebhook": "查看 Webhook 链接",
|
||||
"webhookCopied": "Webhook URL 已复制到剪贴板",
|
||||
"webhookCopyFailed": "复制失败,请手动复制",
|
||||
"webhookDialog": {
|
||||
"title": "Webhook 回调地址",
|
||||
"description": "回调地址如下,请确保网络环境可以公网访问。也可以在日志中查看回调地址信息。建议填写 配置文件 -> 系统 中的「对外可达的回调接口地址」配置项。",
|
||||
"close": "关闭"
|
||||
},
|
||||
"details": {
|
||||
"adapterType": "适配器类型",
|
||||
"token": "Token",
|
||||
|
||||
@@ -29,6 +29,20 @@
|
||||
<item-card :item="platform" title-field="id" enabled-field="enable"
|
||||
:bglogo="getPlatformIcon(platform.type || platform.id)" @toggle-enabled="platformStatusChange"
|
||||
@delete="deletePlatform" @edit="editPlatform">
|
||||
<template #item-details="{ item }">
|
||||
<div v-if="item.unified_webhook_mode && item.webhook_uuid" class="webhook-info">
|
||||
<v-chip
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="webhook-chip"
|
||||
@click.stop="openWebhookDialog(item.webhook_uuid)"
|
||||
>
|
||||
<v-icon size="small" start>mdi-webhook</v-icon>
|
||||
{{ tm('viewWebhook') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
</item-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -60,6 +74,43 @@
|
||||
:updating-mode="updatingMode" :updating-platform-config="updatingPlatformConfig" @update="getConfig"
|
||||
@show-toast="showToast" @refresh-config="getConfig"/>
|
||||
|
||||
<!-- Webhook URL 对话框 -->
|
||||
<v-dialog v-model="showWebhookDialog" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<v-icon class="me-2" color="primary">mdi-webhook</v-icon>
|
||||
{{ tm('webhookDialog.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text class="px-4 pb-2">
|
||||
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('webhookDialog.description') }}</p>
|
||||
<v-text-field
|
||||
:model-value="currentWebhookUrl"
|
||||
readonly
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="webhook-url-field"
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="copyWebhookUrl(currentWebhookUuid)"
|
||||
>
|
||||
<v-icon>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4 pt-2">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="tonal" color="primary" @click="showWebhookDialog = false">
|
||||
{{ tm('webhookDialog.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack"
|
||||
location="top">
|
||||
@@ -97,18 +148,6 @@ export default {
|
||||
tm
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 安全访问翻译的计算属性
|
||||
messages() {
|
||||
return {
|
||||
updateSuccess: this.tm('messages.updateSuccess'),
|
||||
addSuccess: this.tm('messages.addSuccess'),
|
||||
deleteSuccess: this.tm('messages.deleteSuccess'),
|
||||
statusUpdateSuccess: this.tm('messages.statusUpdateSuccess'),
|
||||
deleteConfirm: this.tm('messages.deleteConfirm')
|
||||
};
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config_data: {},
|
||||
@@ -125,6 +164,9 @@ export default {
|
||||
|
||||
showConsole: false,
|
||||
|
||||
showWebhookDialog: false,
|
||||
currentWebhookUuid: '',
|
||||
|
||||
store: useCommonStore()
|
||||
}
|
||||
},
|
||||
@@ -224,6 +266,47 @@ export default {
|
||||
this.save_message = message;
|
||||
this.save_message_success = "error";
|
||||
this.save_message_snack = true;
|
||||
},
|
||||
|
||||
getWebhookUrl(webhookUuid) {
|
||||
let callbackBase = this.config_data.callback_api_base || '';
|
||||
if (!callbackBase) {
|
||||
callbackBase = "http(s)://<your-domain-or-ip>";
|
||||
}
|
||||
if (callbackBase) {
|
||||
return `${callbackBase.replace(/\/$/, '')}/api/platform/webhook/${webhookUuid}`;
|
||||
}
|
||||
return `/api/platform/webhook/${webhookUuid}`;
|
||||
},
|
||||
|
||||
openWebhookDialog(webhookUuid) {
|
||||
this.currentWebhookUuid = webhookUuid;
|
||||
this.showWebhookDialog = true;
|
||||
},
|
||||
|
||||
async copyWebhookUrl(webhookUuid) {
|
||||
const url = this.getWebhookUrl(webhookUuid);
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
this.showSuccess(this.tm('webhookCopied'));
|
||||
} catch (err) {
|
||||
this.showError(this.tm('webhookCopyFailed'));
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 安全访问翻译的计算属性
|
||||
messages() {
|
||||
return {
|
||||
updateSuccess: this.tm('messages.updateSuccess'),
|
||||
addSuccess: this.tm('messages.addSuccess'),
|
||||
deleteSuccess: this.tm('messages.deleteSuccess'),
|
||||
statusUpdateSuccess: this.tm('messages.statusUpdateSuccess'),
|
||||
deleteConfirm: this.tm('messages.deleteConfirm')
|
||||
};
|
||||
},
|
||||
currentWebhookUrl() {
|
||||
return this.getWebhookUrl(this.currentWebhookUuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,4 +318,12 @@ export default {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.webhook-info {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.webhook-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user