AstrBot 4.7.4__py3-none-any.whl → 4.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/astr_agent_run_util.py +15 -1
  3. astrbot/core/config/default.py +58 -1
  4. astrbot/core/db/__init__.py +30 -1
  5. astrbot/core/db/sqlite.py +55 -1
  6. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
  7. astrbot/core/platform/manager.py +67 -9
  8. astrbot/core/platform/platform.py +99 -2
  9. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +4 -3
  10. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +4 -6
  11. astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
  12. astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
  13. astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
  14. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
  15. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
  16. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
  17. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
  18. astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
  19. astrbot/core/platform/sources/slack/client.py +50 -39
  20. astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
  21. astrbot/core/platform/sources/slack/slack_event.py +3 -3
  22. astrbot/core/platform/sources/telegram/tg_adapter.py +1 -2
  23. astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
  24. astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
  25. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
  26. astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
  27. astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
  28. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
  29. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
  30. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
  31. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
  32. astrbot/core/platform_message_history_mgr.py +3 -3
  33. astrbot/core/provider/sources/whisper_api_source.py +43 -11
  34. astrbot/core/utils/tencent_record_helper.py +1 -1
  35. astrbot/core/utils/webhook_utils.py +47 -0
  36. astrbot/dashboard/routes/__init__.py +2 -0
  37. astrbot/dashboard/routes/chat.py +300 -70
  38. astrbot/dashboard/routes/config.py +19 -0
  39. astrbot/dashboard/routes/knowledge_base.py +1 -1
  40. astrbot/dashboard/routes/platform.py +100 -0
  41. astrbot/dashboard/server.py +3 -1
  42. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
  43. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/RECORD +46 -44
  44. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
  45. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
  46. {astrbot-4.7.4.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@ import asyncio
2
2
  import os
3
3
  import sys
4
4
  import uuid
5
+ from typing import Any
5
6
 
6
7
  import quart
7
8
  from requests import Response
@@ -24,6 +25,7 @@ from astrbot.api.platform import (
24
25
  from astrbot.core import logger
25
26
  from astrbot.core.platform.astr_message_event import MessageSesion
26
27
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
28
+ from astrbot.core.utils.webhook_utils import log_webhook_info
27
29
 
28
30
  from .wecom_event import WecomPlatformEvent
29
31
  from .wecom_kf import WeChatKF
@@ -62,8 +64,20 @@ class WecomServer:
62
64
  self.shutdown_event = asyncio.Event()
63
65
 
64
66
  async def verify(self):
65
- logger.info(f"验证请求有效性: {quart.request.args}")
66
- args = quart.request.args
67
+ """内部服务器的 GET 验证入口"""
68
+ return await self.handle_verify(quart.request)
69
+
70
+ async def handle_verify(self, request) -> str:
71
+ """处理验证请求,可被统一 webhook 入口复用
72
+
73
+ Args:
74
+ request: Quart 请求对象
75
+
76
+ Returns:
77
+ 验证响应
78
+ """
79
+ logger.info(f"验证请求有效性: {request.args}")
80
+ args = request.args
67
81
  try:
68
82
  echo_str = self.crypto.check_signature(
69
83
  args.get("msg_signature"),
@@ -78,10 +92,22 @@ class WecomServer:
78
92
  raise
79
93
 
80
94
  async def callback_command(self):
81
- data = await quart.request.get_data()
82
- msg_signature = quart.request.args.get("msg_signature")
83
- timestamp = quart.request.args.get("timestamp")
84
- nonce = quart.request.args.get("nonce")
95
+ """内部服务器的 POST 回调入口"""
96
+ return await self.handle_callback(quart.request)
97
+
98
+ async def handle_callback(self, request) -> str:
99
+ """处理回调请求,可被统一 webhook 入口复用
100
+
101
+ Args:
102
+ request: Quart 请求对象
103
+
104
+ Returns:
105
+ 响应内容
106
+ """
107
+ data = await request.get_data()
108
+ msg_signature = request.args.get("msg_signature")
109
+ timestamp = request.args.get("timestamp")
110
+ nonce = request.args.get("nonce")
85
111
  try:
86
112
  xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
87
113
  except InvalidSignatureException:
@@ -118,14 +144,14 @@ class WecomPlatformAdapter(Platform):
118
144
  platform_settings: dict,
119
145
  event_queue: asyncio.Queue,
120
146
  ) -> None:
121
- super().__init__(event_queue)
122
- self.config = platform_config
147
+ super().__init__(platform_config, event_queue)
123
148
  self.settingss = platform_settings
124
149
  self.client_self_id = uuid.uuid4().hex[:8]
125
150
  self.api_base_url = platform_config.get(
126
151
  "api_base_url",
127
152
  "https://qyapi.weixin.qq.com/cgi-bin/",
128
153
  )
154
+ self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
129
155
 
130
156
  if not self.api_base_url:
131
157
  self.api_base_url = "https://qyapi.weixin.qq.com/cgi-bin/"
@@ -232,7 +258,23 @@ class WecomPlatformAdapter(Platform):
232
258
  )
233
259
  except Exception as e:
234
260
  logger.error(e)
235
- await self.server.start_polling()
261
+
262
+ # 如果启用统一 webhook 模式,则不启动独立服务器
263
+ webhook_uuid = self.config.get("webhook_uuid")
264
+ if self.unified_webhook_mode and webhook_uuid:
265
+ log_webhook_info(f"{self.meta().id}(企业微信)", webhook_uuid)
266
+ # 保持运行状态,等待 shutdown
267
+ await self.server.shutdown_event.wait()
268
+ else:
269
+ await self.server.start_polling()
270
+
271
+ async def webhook_callback(self, request: Any) -> Any:
272
+ """统一 Webhook 回调入口"""
273
+ # 根据请求方法分发到不同的处理函数
274
+ if request.method == "GET":
275
+ return await self.server.handle_verify(request)
276
+ else:
277
+ return await self.server.handle_callback(request)
236
278
 
237
279
  async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None:
238
280
  abm = AstrBotMessage()
@@ -16,7 +16,7 @@ try:
16
16
  import pydub
17
17
  except Exception:
18
18
  logger.warning(
19
- "检测到 pydub 库未安装,企业微信将无法语音收发。如需使用语音,请前往管理面板 -> 控制台 -> 安装 Pip 库安装 pydub。",
19
+ "检测到 pydub 库未安装,企业微信将无法语音收发。如需使用语音,请前往管理面板 -> 平台日志 -> 安装 Pip 库安装 pydub。",
20
20
  )
21
21
 
22
22
 
@@ -22,6 +22,7 @@ from astrbot.api.platform import (
22
22
  PlatformMetadata,
23
23
  )
24
24
  from astrbot.core.platform.astr_message_event import MessageSesion
25
+ from astrbot.core.utils.webhook_utils import log_webhook_info
25
26
 
26
27
  from ...register import register_platform_adapter
27
28
  from .wecomai_api import (
@@ -103,9 +104,7 @@ class WecomAIBotAdapter(Platform):
103
104
  platform_settings: dict,
104
105
  event_queue: asyncio.Queue,
105
106
  ) -> None:
106
- super().__init__(event_queue)
107
-
108
- self.config = platform_config
107
+ super().__init__(platform_config, event_queue)
109
108
  self.settings = platform_settings
110
109
 
111
110
  # 初始化配置参数
@@ -122,6 +121,7 @@ class WecomAIBotAdapter(Platform):
122
121
  "wecomaibot_friend_message_welcome_text",
123
122
  "",
124
123
  )
124
+ self.unified_webhook_mode = self.config.get("unified_webhook_mode", False)
125
125
 
126
126
  # 平台元数据
127
127
  self.metadata = PlatformMetadata(
@@ -425,17 +425,34 @@ class WecomAIBotAdapter(Platform):
425
425
 
426
426
  def run(self) -> Awaitable[Any]:
427
427
  """运行适配器,同时启动HTTP服务器和队列监听器"""
428
- logger.info("启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port)
429
428
 
430
429
  async def run_both():
431
- # 同时运行HTTP服务器和队列监听器
432
- await asyncio.gather(
433
- self.server.start_server(),
434
- self.queue_listener.run(),
435
- )
430
+ # 如果启用统一 webhook 模式,则不启动独立服务器
431
+ webhook_uuid = self.config.get("webhook_uuid")
432
+ if self.unified_webhook_mode and webhook_uuid:
433
+ log_webhook_info(f"{self.meta().id}(企业微信智能机器人)", webhook_uuid)
434
+ # 只运行队列监听器
435
+ await self.queue_listener.run()
436
+ else:
437
+ logger.info(
438
+ "启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port
439
+ )
440
+ # 同时运行HTTP服务器和队列监听器
441
+ await asyncio.gather(
442
+ self.server.start_server(),
443
+ self.queue_listener.run(),
444
+ )
436
445
 
437
446
  return run_both()
438
447
 
448
+ async def webhook_callback(self, request: Any) -> Any:
449
+ """统一 Webhook 回调入口"""
450
+ # 根据请求方法分发到不同的处理函数
451
+ if request.method == "GET":
452
+ return await self.server.handle_verify(request)
453
+ else:
454
+ return await self.server.handle_callback(request)
455
+
439
456
  async def terminate(self):
440
457
  """终止适配器"""
441
458
  logger.info("企业微信智能机器人适配器正在关闭...")
@@ -59,8 +59,19 @@ class WecomAIBotServer:
59
59
  )
60
60
 
61
61
  async def verify_url(self):
62
- """验证回调 URL"""
63
- args = quart.request.args
62
+ """内部服务器的 GET 验证入口"""
63
+ return await self.handle_verify(quart.request)
64
+
65
+ async def handle_verify(self, request):
66
+ """处理 URL 验证请求,可被统一 webhook 入口复用
67
+
68
+ Args:
69
+ request: Quart 请求对象
70
+
71
+ Returns:
72
+ 验证响应元组 (content, status_code, headers)
73
+ """
74
+ args = request.args
64
75
  msg_signature = args.get("msg_signature")
65
76
  timestamp = args.get("timestamp")
66
77
  nonce = args.get("nonce")
@@ -81,8 +92,19 @@ class WecomAIBotServer:
81
92
  return result, 200, {"Content-Type": "text/plain"}
82
93
 
83
94
  async def handle_message(self):
84
- """处理消息回调"""
85
- args = quart.request.args
95
+ """内部服务器的 POST 消息回调入口"""
96
+ return await self.handle_callback(quart.request)
97
+
98
+ async def handle_callback(self, request):
99
+ """处理消息回调,可被统一 webhook 入口复用
100
+
101
+ Args:
102
+ request: Quart 请求对象
103
+
104
+ Returns:
105
+ 响应元组 (content, status_code, headers)
106
+ """
107
+ args = request.args
86
108
  msg_signature = args.get("msg_signature")
87
109
  timestamp = args.get("timestamp")
88
110
  nonce = args.get("nonce")
@@ -102,7 +124,7 @@ class WecomAIBotServer:
102
124
 
103
125
  try:
104
126
  # 获取请求体
105
- post_data = await quart.request.get_data()
127
+ post_data = await request.get_data()
106
128
 
107
129
  # 确保 post_data 是 bytes 类型
108
130
  if isinstance(post_data, str):
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import sys
3
3
  import uuid
4
+ from typing import Any
4
5
 
5
6
  import quart
6
7
  from requests import Response
@@ -22,6 +23,7 @@ from astrbot.api.platform import (
22
23
  )
23
24
  from astrbot.core import logger
24
25
  from astrbot.core.platform.astr_message_event import MessageSesion
26
+ from astrbot.core.utils.webhook_utils import log_webhook_info
25
27
 
26
28
  from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent
27
29
 
@@ -31,7 +33,7 @@ else:
31
33
  from typing_extensions import override
32
34
 
33
35
 
34
- class WecomServer:
36
+ class WeixinOfficialAccountServer:
35
37
  def __init__(self, event_queue: asyncio.Queue, config: dict):
36
38
  self.server = quart.Quart(__name__)
37
39
  self.port = int(config.get("port"))
@@ -57,9 +59,21 @@ class WecomServer:
57
59
  self.shutdown_event = asyncio.Event()
58
60
 
59
61
  async def verify(self):
60
- logger.info(f"验证请求有效性: {quart.request.args}")
62
+ """内部服务器的 GET 验证入口"""
63
+ return await self.handle_verify(quart.request)
61
64
 
62
- args = quart.request.args
65
+ async def handle_verify(self, request) -> str:
66
+ """处理验证请求,可被统一 webhook 入口复用
67
+
68
+ Args:
69
+ request: Quart 请求对象
70
+
71
+ Returns:
72
+ 验证响应
73
+ """
74
+ logger.info(f"验证请求有效性: {request.args}")
75
+
76
+ args = request.args
63
77
  if not args.get("signature", None):
64
78
  logger.error("未知的响应,请检查回调地址是否填写正确。")
65
79
  return "err"
@@ -77,10 +91,22 @@ class WecomServer:
77
91
  return "err"
78
92
 
79
93
  async def callback_command(self):
80
- data = await quart.request.get_data()
81
- msg_signature = quart.request.args.get("msg_signature")
82
- timestamp = quart.request.args.get("timestamp")
83
- nonce = quart.request.args.get("nonce")
94
+ """内部服务器的 POST 回调入口"""
95
+ return await self.handle_callback(quart.request)
96
+
97
+ async def handle_callback(self, request) -> str:
98
+ """处理回调请求,可被统一 webhook 入口复用
99
+
100
+ Args:
101
+ request: Quart 请求对象
102
+
103
+ Returns:
104
+ 响应内容
105
+ """
106
+ data = await request.get_data()
107
+ msg_signature = request.args.get("msg_signature")
108
+ timestamp = request.args.get("timestamp")
109
+ nonce = request.args.get("nonce")
84
110
  try:
85
111
  xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce)
86
112
  except InvalidSignatureException:
@@ -123,8 +149,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
123
149
  platform_settings: dict,
124
150
  event_queue: asyncio.Queue,
125
151
  ) -> None:
126
- super().__init__(event_queue)
127
- self.config = platform_config
152
+ super().__init__(platform_config, event_queue)
128
153
  self.settingss = platform_settings
129
154
  self.client_self_id = uuid.uuid4().hex[:8]
130
155
  self.api_base_url = platform_config.get(
@@ -132,6 +157,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
132
157
  "https://api.weixin.qq.com/cgi-bin/",
133
158
  )
134
159
  self.active_send_mode = self.config.get("active_send_mode", False)
160
+ self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False)
135
161
 
136
162
  if not self.api_base_url:
137
163
  self.api_base_url = "https://api.weixin.qq.com/cgi-bin/"
@@ -143,7 +169,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
143
169
  if not self.api_base_url.endswith("/"):
144
170
  self.api_base_url += "/"
145
171
 
146
- self.server = WecomServer(self._event_queue, self.config)
172
+ self.server = WeixinOfficialAccountServer(self._event_queue, self.config)
147
173
 
148
174
  self.client = WeChatClient(
149
175
  self.config["appid"].strip(),
@@ -202,7 +228,22 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
202
228
 
203
229
  @override
204
230
  async def run(self):
205
- await self.server.start_polling()
231
+ # 如果启用统一 webhook 模式,则不启动独立服务器
232
+ webhook_uuid = self.config.get("webhook_uuid")
233
+ if self.unified_webhook_mode and webhook_uuid:
234
+ log_webhook_info(f"{self.meta().id}(微信公众平台)", webhook_uuid)
235
+ # 保持运行状态,等待 shutdown
236
+ await self.server.shutdown_event.wait()
237
+ else:
238
+ await self.server.start_polling()
239
+
240
+ async def webhook_callback(self, request: Any) -> Any:
241
+ """统一 Webhook 回调入口"""
242
+ # 根据请求方法分发到不同的处理函数
243
+ if request.method == "GET":
244
+ return await self.server.handle_verify(request)
245
+ else:
246
+ return await self.server.handle_callback(request)
206
247
 
207
248
  async def convert_message(
208
249
  self,
@@ -13,7 +13,7 @@ try:
13
13
  import pydub
14
14
  except Exception:
15
15
  logger.warning(
16
- "检测到 pydub 库未安装,微信公众平台将无法语音收发。如需使用语音,请前往管理面板 -> 控制台 -> 安装 Pip 库安装 pydub。",
16
+ "检测到 pydub 库未安装,微信公众平台将无法语音收发。如需使用语音,请前往管理面板 -> 平台日志 -> 安装 Pip 库安装 pydub。",
17
17
  )
18
18
 
19
19
 
@@ -10,12 +10,12 @@ class PlatformMessageHistoryManager:
10
10
  self,
11
11
  platform_id: str,
12
12
  user_id: str,
13
- content: list[dict], # TODO: parse from message chain
13
+ content: dict, # TODO: parse from message chain
14
14
  sender_id: str | None = None,
15
15
  sender_name: str | None = None,
16
- ):
16
+ ) -> PlatformMessageHistory:
17
17
  """Insert a new platform message history record."""
18
- await self.db.insert_platform_message_history(
18
+ return await self.db.insert_platform_message_history(
19
19
  platform_id=platform_id,
20
20
  user_id=user_id,
21
21
  content=content,
@@ -6,7 +6,10 @@ from openai import NOT_GIVEN, AsyncOpenAI
6
6
  from astrbot.core import logger
7
7
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
8
8
  from astrbot.core.utils.io import download_file
9
- from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav
9
+ from astrbot.core.utils.tencent_record_helper import (
10
+ convert_to_pcm_wav,
11
+ tencent_silk_to_wav,
12
+ )
10
13
 
11
14
  from ..entities import ProviderType
12
15
  from ..provider import STTProvider
@@ -35,18 +38,28 @@ class ProviderOpenAIWhisperAPI(STTProvider):
35
38
 
36
39
  self.set_model(provider_config.get("model"))
37
40
 
38
- async def _is_silk_file(self, file_path):
41
+ async def _get_audio_format(self, file_path):
42
+ # 定义要检测的头部字节
39
43
  silk_header = b"SILK"
40
- with open(file_path, "rb") as f:
41
- file_header = f.read(8)
44
+ amr_header = b"#!AMR"
45
+
46
+ try:
47
+ with open(file_path, "rb") as f:
48
+ file_header = f.read(8)
49
+ except FileNotFoundError:
50
+ return None
42
51
 
43
52
  if silk_header in file_header:
44
- return True
45
- return False
53
+ return "silk"
54
+
55
+ if amr_header in file_header:
56
+ return "amr"
57
+ return None
46
58
 
47
59
  async def get_text(self, audio_url: str) -> str:
48
60
  """Only supports mp3, mp4, mpeg, m4a, wav, webm"""
49
61
  is_tencent = False
62
+ output_path = None
50
63
 
51
64
  if audio_url.startswith("http"):
52
65
  if "multimedia.nt.qq.com.cn" in audio_url:
@@ -62,16 +75,35 @@ class ProviderOpenAIWhisperAPI(STTProvider):
62
75
  raise FileNotFoundError(f"文件不存在: {audio_url}")
63
76
 
64
77
  if audio_url.endswith(".amr") or audio_url.endswith(".silk") or is_tencent:
65
- is_silk = await self._is_silk_file(audio_url)
66
- if is_silk:
67
- logger.info("Converting silk file to wav ...")
78
+ file_format = await self._get_audio_format(audio_url)
79
+
80
+ # 判断是否需要转换
81
+ if file_format in ["silk", "amr"]:
68
82
  temp_dir = os.path.join(get_astrbot_data_path(), "temp")
69
83
  output_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".wav")
70
- await tencent_silk_to_wav(audio_url, output_path)
84
+
85
+ if file_format == "silk":
86
+ logger.info(
87
+ "Converting silk file to wav using tencent_silk_to_wav..."
88
+ )
89
+ await tencent_silk_to_wav(audio_url, output_path)
90
+ elif file_format == "amr":
91
+ logger.info(
92
+ "Converting amr file to wav using convert_to_pcm_wav..."
93
+ )
94
+ await convert_to_pcm_wav(audio_url, output_path)
95
+
71
96
  audio_url = output_path
72
97
 
73
98
  result = await self.client.audio.transcriptions.create(
74
99
  model=self.model_name,
75
- file=open(audio_url, "rb"),
100
+ file=("audio.wav", open(audio_url, "rb")),
76
101
  )
102
+
103
+ # remove temp file
104
+ if output_path and os.path.exists(output_path):
105
+ try:
106
+ os.remove(audio_url)
107
+ except Exception as e:
108
+ logger.error(f"Failed to remove temp file {audio_url}: {e}")
77
109
  return result.text
@@ -36,7 +36,7 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> int:
36
36
  import pilk
37
37
  except (ImportError, ModuleNotFoundError) as _:
38
38
  raise Exception(
39
- "pilk 模块未安装,请前往管理面板->控制台->安装pip库 安装 pilk 这个库",
39
+ "pilk 模块未安装,请前往管理面板->平台日志->安装pip库 安装 pilk 这个库",
40
40
  )
41
41
  # with wave.open(wav_path, 'rb') as wav:
42
42
  # wav_data = wav.readframes(wav.getnframes())
@@ -0,0 +1,47 @@
1
+ from astrbot.core import astrbot_config, logger
2
+
3
+
4
+ def _get_callback_api_base() -> str:
5
+ try:
6
+ return astrbot_config.get("callback_api_base", "").rstrip("/")
7
+ except Exception as e:
8
+ logger.error(f"获取 callback_api_base 失败: {e!s}")
9
+ return ""
10
+
11
+
12
+ def _get_dashboard_port() -> int:
13
+ try:
14
+ return astrbot_config.get("dashboard", {}).get("port", 6185)
15
+ except Exception as e:
16
+ logger.error(f"获取 dashboard 端口失败: {e!s}")
17
+ return 6185
18
+
19
+
20
+ def log_webhook_info(platform_name: str, webhook_uuid: str):
21
+ """打印美观的 webhook 信息日志
22
+
23
+ Args:
24
+ platform_name: 平台名称
25
+ webhook_uuid: webhook 的 UUID
26
+ """
27
+
28
+ callback_base = _get_callback_api_base()
29
+
30
+ if not callback_base:
31
+ callback_base = "http(s)://<your-astrbot-domain>"
32
+
33
+ if not callback_base.startswith("http"):
34
+ callback_base = f"http(s)://{callback_base}"
35
+
36
+ callback_base = callback_base.rstrip("/")
37
+ webhook_url = f"{callback_base}/api/platform/webhook/{webhook_uuid}"
38
+
39
+ display_log = (
40
+ "\n====================\n"
41
+ f"🔗 机器人平台 {platform_name} 已启用统一 Webhook 模式\n"
42
+ f"📍 Webhook 回调地址: \n"
43
+ f" ➜ http://<your-ip>:{_get_dashboard_port()}/api/platform/webhook/{webhook_uuid}\n"
44
+ f" ➜ {webhook_url}\n"
45
+ "====================\n"
46
+ )
47
+ logger.info(display_log)
@@ -6,6 +6,7 @@ from .file import FileRoute
6
6
  from .knowledge_base import KnowledgeBaseRoute
7
7
  from .log import LogRoute
8
8
  from .persona import PersonaRoute
9
+ from .platform import PlatformRoute
9
10
  from .plugin import PluginRoute
10
11
  from .session_management import SessionManagementRoute
11
12
  from .stat import StatRoute
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "KnowledgeBaseRoute",
23
24
  "LogRoute",
24
25
  "PersonaRoute",
26
+ "PlatformRoute",
25
27
  "PluginRoute",
26
28
  "SessionManagementRoute",
27
29
  "StatRoute",