AstrBot 4.7.3__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 (52) hide show
  1. astrbot/cli/__init__.py +1 -1
  2. astrbot/core/agent/message.py +21 -5
  3. astrbot/core/astr_agent_run_util.py +15 -1
  4. astrbot/core/config/default.py +113 -1
  5. astrbot/core/db/__init__.py +30 -1
  6. astrbot/core/db/sqlite.py +55 -1
  7. astrbot/core/message/components.py +6 -1
  8. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +64 -5
  9. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +1 -1
  10. astrbot/core/platform/manager.py +67 -9
  11. astrbot/core/platform/platform.py +99 -2
  12. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +19 -5
  13. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +5 -7
  14. astrbot/core/platform/sources/discord/discord_platform_adapter.py +1 -2
  15. astrbot/core/platform/sources/lark/lark_adapter.py +1 -3
  16. astrbot/core/platform/sources/misskey/misskey_adapter.py +1 -2
  17. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +2 -0
  18. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -3
  19. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +32 -9
  20. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +13 -1
  21. astrbot/core/platform/sources/satori/satori_adapter.py +1 -2
  22. astrbot/core/platform/sources/slack/client.py +50 -39
  23. astrbot/core/platform/sources/slack/slack_adapter.py +21 -7
  24. astrbot/core/platform/sources/slack/slack_event.py +3 -3
  25. astrbot/core/platform/sources/telegram/tg_adapter.py +4 -3
  26. astrbot/core/platform/sources/webchat/webchat_adapter.py +95 -29
  27. astrbot/core/platform/sources/webchat/webchat_event.py +33 -33
  28. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +1 -2
  29. astrbot/core/platform/sources/wecom/wecom_adapter.py +51 -9
  30. astrbot/core/platform/sources/wecom/wecom_event.py +1 -1
  31. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +26 -9
  32. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +27 -5
  33. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +52 -11
  34. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +1 -1
  35. astrbot/core/platform_message_history_mgr.py +3 -3
  36. astrbot/core/provider/provider.py +35 -0
  37. astrbot/core/provider/sources/whisper_api_source.py +43 -11
  38. astrbot/core/utils/file_extract.py +23 -0
  39. astrbot/core/utils/tencent_record_helper.py +1 -1
  40. astrbot/core/utils/webhook_utils.py +47 -0
  41. astrbot/dashboard/routes/__init__.py +2 -0
  42. astrbot/dashboard/routes/chat.py +300 -70
  43. astrbot/dashboard/routes/config.py +32 -165
  44. astrbot/dashboard/routes/knowledge_base.py +1 -1
  45. astrbot/dashboard/routes/platform.py +100 -0
  46. astrbot/dashboard/routes/plugin.py +65 -6
  47. astrbot/dashboard/server.py +3 -1
  48. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/METADATA +48 -37
  49. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/RECORD +52 -49
  50. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/WHEEL +0 -0
  51. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/entry_points.txt +0 -0
  52. {astrbot-4.7.3.dist-info → astrbot-4.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,
@@ -1,5 +1,6 @@
1
1
  import abc
2
2
  import asyncio
3
+ import os
3
4
  from collections.abc import AsyncGenerator
4
5
 
5
6
  from astrbot.core.agent.message import Message
@@ -11,6 +12,7 @@ from astrbot.core.provider.entities import (
11
12
  ToolCallsResult,
12
13
  )
13
14
  from astrbot.core.provider.register import provider_cls_map
15
+ from astrbot.core.utils.astrbot_path import get_astrbot_path
14
16
 
15
17
 
16
18
  class AbstractProvider(abc.ABC):
@@ -43,6 +45,14 @@ class AbstractProvider(abc.ABC):
43
45
  )
44
46
  return meta
45
47
 
48
+ async def test(self):
49
+ """test the provider is a
50
+
51
+ raises:
52
+ Exception: if the provider is not available
53
+ """
54
+ ...
55
+
46
56
 
47
57
  class Provider(AbstractProvider):
48
58
  """Chat Provider"""
@@ -165,6 +175,12 @@ class Provider(AbstractProvider):
165
175
 
166
176
  return dicts
167
177
 
178
+ async def test(self, timeout: float = 45.0):
179
+ await asyncio.wait_for(
180
+ self.text_chat(prompt="REPLY `PONG` ONLY"),
181
+ timeout=timeout,
182
+ )
183
+
168
184
 
169
185
  class STTProvider(AbstractProvider):
170
186
  def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -177,6 +193,14 @@ class STTProvider(AbstractProvider):
177
193
  """获取音频的文本"""
178
194
  raise NotImplementedError
179
195
 
196
+ async def test(self):
197
+ sample_audio_path = os.path.join(
198
+ get_astrbot_path(),
199
+ "samples",
200
+ "stt_health_check.wav",
201
+ )
202
+ await self.get_text(sample_audio_path)
203
+
180
204
 
181
205
  class TTSProvider(AbstractProvider):
182
206
  def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -189,6 +213,9 @@ class TTSProvider(AbstractProvider):
189
213
  """获取文本的音频,返回音频文件路径"""
190
214
  raise NotImplementedError
191
215
 
216
+ async def test(self):
217
+ await self.get_audio("hi")
218
+
192
219
 
193
220
  class EmbeddingProvider(AbstractProvider):
194
221
  def __init__(self, provider_config: dict, provider_settings: dict) -> None:
@@ -211,6 +238,9 @@ class EmbeddingProvider(AbstractProvider):
211
238
  """获取向量的维度"""
212
239
  ...
213
240
 
241
+ async def test(self):
242
+ await self.get_embedding("astrbot")
243
+
214
244
  async def get_embeddings_batch(
215
245
  self,
216
246
  texts: list[str],
@@ -294,3 +324,8 @@ class RerankProvider(AbstractProvider):
294
324
  ) -> list[RerankResult]:
295
325
  """获取查询和文档的重排序分数"""
296
326
  ...
327
+
328
+ async def test(self):
329
+ result = await self.rerank("Apple", documents=["apple", "banana"])
330
+ if not result:
331
+ raise Exception("Rerank provider test failed, no results returned")
@@ -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
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+
3
+ from openai import AsyncOpenAI
4
+
5
+
6
+ async def extract_file_moonshotai(file_path: str, api_key: str) -> str:
7
+ """Extract text from a file using Moonshot AI API"""
8
+ """
9
+ Args:
10
+ file_path: The path to the file to extract text from
11
+ api_key: The API key to use to extract text from the file
12
+ Returns:
13
+ The text extracted from the file
14
+ """
15
+ client = AsyncOpenAI(
16
+ api_key=api_key,
17
+ base_url="https://api.moonshot.cn/v1",
18
+ )
19
+ file_object = await client.files.create(
20
+ file=Path(file_path),
21
+ purpose="file-extract", # type: ignore
22
+ )
23
+ return (await client.files.content(file_id=file_object.id)).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",