AstrBot 4.0.0b5__py3-none-any.whl → 4.1.1__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 (33) hide show
  1. astrbot/api/event/filter/__init__.py +2 -0
  2. astrbot/core/config/default.py +73 -3
  3. astrbot/core/initial_loader.py +4 -1
  4. astrbot/core/message/components.py +59 -50
  5. astrbot/core/pipeline/result_decorate/stage.py +5 -1
  6. astrbot/core/platform/manager.py +25 -3
  7. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +11 -4
  8. astrbot/core/platform/sources/satori/satori_adapter.py +482 -0
  9. astrbot/core/platform/sources/satori/satori_event.py +221 -0
  10. astrbot/core/platform/sources/telegram/tg_adapter.py +0 -1
  11. astrbot/core/provider/sources/openai_source.py +14 -5
  12. astrbot/core/provider/sources/vllm_rerank_source.py +6 -0
  13. astrbot/core/star/__init__.py +7 -5
  14. astrbot/core/star/filter/command.py +9 -3
  15. astrbot/core/star/filter/platform_adapter_type.py +3 -0
  16. astrbot/core/star/register/__init__.py +2 -0
  17. astrbot/core/star/register/star_handler.py +18 -4
  18. astrbot/core/star/star_handler.py +9 -1
  19. astrbot/core/star/star_tools.py +116 -21
  20. astrbot/core/utils/t2i/network_strategy.py +11 -18
  21. astrbot/core/utils/t2i/renderer.py +8 -2
  22. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  23. astrbot/core/utils/t2i/template_manager.py +112 -0
  24. astrbot/dashboard/routes/chat.py +6 -1
  25. astrbot/dashboard/routes/config.py +10 -49
  26. astrbot/dashboard/routes/route.py +19 -2
  27. astrbot/dashboard/routes/t2i.py +230 -0
  28. astrbot/dashboard/server.py +13 -4
  29. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/METADATA +39 -52
  30. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/RECORD +33 -28
  31. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/WHEEL +0 -0
  32. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/entry_points.txt +0 -0
  33. {astrbot-4.0.0b5.dist-info → astrbot-4.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,221 @@
1
+ from typing import TYPE_CHECKING
2
+ from astrbot.api import logger
3
+ from astrbot.api.event import AstrMessageEvent, MessageChain
4
+ from astrbot.api.platform import AstrBotMessage, PlatformMetadata
5
+ from astrbot.api.message_components import Plain, Image, At, File, Record
6
+
7
+ if TYPE_CHECKING:
8
+ from .satori_adapter import SatoriPlatformAdapter
9
+
10
+
11
+ class SatoriPlatformEvent(AstrMessageEvent):
12
+ def __init__(
13
+ self,
14
+ message_str: str,
15
+ message_obj: AstrBotMessage,
16
+ platform_meta: PlatformMetadata,
17
+ session_id: str,
18
+ adapter: "SatoriPlatformAdapter",
19
+ ):
20
+ super().__init__(message_str, message_obj, platform_meta, session_id)
21
+ self.adapter = adapter
22
+ self.platform = None
23
+ self.user_id = None
24
+ if (
25
+ hasattr(message_obj, "raw_message")
26
+ and message_obj.raw_message
27
+ and isinstance(message_obj.raw_message, dict)
28
+ ):
29
+ login = message_obj.raw_message.get("login", {})
30
+ self.platform = login.get("platform")
31
+ user = login.get("user", {})
32
+ self.user_id = user.get("id") if user else None
33
+
34
+ @classmethod
35
+ async def send_with_adapter(
36
+ cls, adapter: "SatoriPlatformAdapter", message: MessageChain, session_id: str
37
+ ):
38
+ try:
39
+ content_parts = []
40
+
41
+ for component in message.chain:
42
+ if isinstance(component, Plain):
43
+ text = (
44
+ component.text.replace("&", "&")
45
+ .replace("<", "&lt;")
46
+ .replace(">", "&gt;")
47
+ )
48
+ content_parts.append(text)
49
+
50
+ elif isinstance(component, At):
51
+ if component.qq:
52
+ content_parts.append(f'<at id="{component.qq}"/>')
53
+ elif component.name:
54
+ content_parts.append(f'<at name="{component.name}"/>')
55
+
56
+ elif isinstance(component, Image):
57
+ try:
58
+ image_base64 = await component.convert_to_base64()
59
+ if image_base64:
60
+ content_parts.append(
61
+ f'<img src="data:image/jpeg;base64,{image_base64}"/>'
62
+ )
63
+ except Exception as e:
64
+ logger.error(f"图片转换为base64失败: {e}")
65
+
66
+ elif isinstance(component, File):
67
+ content_parts.append(
68
+ f'<file src="{component.file}" name="{component.name or "文件"}"/>'
69
+ )
70
+
71
+ elif isinstance(component, Record):
72
+ try:
73
+ record_base64 = await component.convert_to_base64()
74
+ if record_base64:
75
+ content_parts.append(
76
+ f'<audio src="data:audio/wav;base64,{record_base64}"/>'
77
+ )
78
+ except Exception as e:
79
+ logger.error(f"语音转换为base64失败: {e}")
80
+
81
+ content = "".join(content_parts)
82
+ channel_id = session_id
83
+ data = {"channel_id": channel_id, "content": content}
84
+
85
+ platform = None
86
+ user_id = None
87
+
88
+ if hasattr(adapter, "logins") and adapter.logins:
89
+ current_login = adapter.logins[0]
90
+ platform = current_login.get("platform", "")
91
+ user = current_login.get("user", {})
92
+ user_id = user.get("id", "") if user else ""
93
+
94
+ result = await adapter.send_http_request(
95
+ "POST", "/message.create", data, platform, user_id
96
+ )
97
+ if result:
98
+ return result
99
+ else:
100
+ return None
101
+
102
+ except Exception as e:
103
+ logger.error(f"Satori 消息发送异常: {e}")
104
+ return None
105
+
106
+ async def send(self, message: MessageChain):
107
+ platform = getattr(self, "platform", None)
108
+ user_id = getattr(self, "user_id", None)
109
+
110
+ if not platform or not user_id:
111
+ if hasattr(self.adapter, "logins") and self.adapter.logins:
112
+ current_login = self.adapter.logins[0]
113
+ platform = current_login.get("platform", "")
114
+ user = current_login.get("user", {})
115
+ user_id = user.get("id", "") if user else ""
116
+
117
+ try:
118
+ content_parts = []
119
+
120
+ for component in message.chain:
121
+ if isinstance(component, Plain):
122
+ text = (
123
+ component.text.replace("&", "&amp;")
124
+ .replace("<", "&lt;")
125
+ .replace(">", "&gt;")
126
+ )
127
+ content_parts.append(text)
128
+
129
+ elif isinstance(component, At):
130
+ if component.qq:
131
+ content_parts.append(f'<at id="{component.qq}"/>')
132
+ elif component.name:
133
+ content_parts.append(f'<at name="{component.name}"/>')
134
+
135
+ elif isinstance(component, Image):
136
+ try:
137
+ image_base64 = await component.convert_to_base64()
138
+ if image_base64:
139
+ content_parts.append(
140
+ f'<img src="data:image/jpeg;base64,{image_base64}"/>'
141
+ )
142
+ except Exception as e:
143
+ logger.error(f"图片转换为base64失败: {e}")
144
+
145
+ elif isinstance(component, File):
146
+ content_parts.append(
147
+ f'<file src="{component.file}" name="{component.name or "文件"}"/>'
148
+ )
149
+
150
+ elif isinstance(component, Record):
151
+ try:
152
+ record_base64 = await component.convert_to_base64()
153
+ if record_base64:
154
+ content_parts.append(
155
+ f'<audio src="data:audio/wav;base64,{record_base64}"/>'
156
+ )
157
+ except Exception as e:
158
+ logger.error(f"语音转换为base64失败: {e}")
159
+
160
+ content = "".join(content_parts)
161
+ channel_id = self.session_id
162
+ data = {"channel_id": channel_id, "content": content}
163
+
164
+ result = await self.adapter.send_http_request(
165
+ "POST", "/message.create", data, platform, user_id
166
+ )
167
+ if not result:
168
+ logger.error("Satori 消息发送失败")
169
+ except Exception as e:
170
+ logger.error(f"Satori 消息发送异常: {e}")
171
+
172
+ await super().send(message)
173
+
174
+ async def send_streaming(self, generator, use_fallback: bool = False):
175
+ try:
176
+ content_parts = []
177
+
178
+ async for chain in generator:
179
+ if isinstance(chain, MessageChain):
180
+ if chain.type == "break":
181
+ if content_parts:
182
+ content = "".join(content_parts)
183
+ temp_chain = MessageChain([Plain(text=content)])
184
+ await self.send(temp_chain)
185
+ content_parts = []
186
+ continue
187
+
188
+ for component in chain.chain:
189
+ if isinstance(component, Plain):
190
+ content_parts.append(component.text)
191
+ elif isinstance(component, Image):
192
+ if content_parts:
193
+ content = "".join(content_parts)
194
+ temp_chain = MessageChain([Plain(text=content)])
195
+ await self.send(temp_chain)
196
+ content_parts = []
197
+ try:
198
+ image_base64 = await component.convert_to_base64()
199
+ if image_base64:
200
+ img_chain = MessageChain(
201
+ [
202
+ Plain(
203
+ text=f'<img src="data:image/jpeg;base64,{image_base64}"/>'
204
+ )
205
+ ]
206
+ )
207
+ await self.send(img_chain)
208
+ except Exception as e:
209
+ logger.error(f"图片转换为base64失败: {e}")
210
+ else:
211
+ content_parts.append(str(component))
212
+
213
+ if content_parts:
214
+ content = "".join(content_parts)
215
+ temp_chain = MessageChain([Plain(text=content)])
216
+ await self.send(temp_chain)
217
+
218
+ except Exception as e:
219
+ logger.error(f"Satori 流式消息发送异常: {e}")
220
+
221
+ return await super().send_streaming(generator, use_fallback)
@@ -183,7 +183,6 @@ class TelegramPlatformAdapter(Platform):
183
183
  return None
184
184
 
185
185
  if not re.match(r"^[a-z0-9_]+$", cmd_name) or len(cmd_name) > 32:
186
- logger.debug(f"跳过无法注册的命令: {cmd_name}")
187
186
  return None
188
187
 
189
188
  # Build description.
@@ -99,12 +99,15 @@ class ProviderOpenAIOfficial(Provider):
99
99
  for key in to_del:
100
100
  del payloads[key]
101
101
 
102
- model = payloads.get("model", "")
103
- # 针对 qwen3 非 thinking 模型的特殊处理:非流式调用必须设置 enable_thinking=false
104
- if "qwen3" in model.lower() and "thinking" not in model.lower():
105
- extra_body["enable_thinking"] = False
102
+ # 读取并合并 custom_extra_body 配置
103
+ custom_extra_body = self.provider_config.get("custom_extra_body", {})
104
+ if isinstance(custom_extra_body, dict):
105
+ extra_body.update(custom_extra_body)
106
+
107
+ model = payloads.get("model", "").lower()
108
+
106
109
  # 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
107
- elif model == "deepseek-reasoner" and "tools" in payloads:
110
+ if model == "deepseek-reasoner" and "tools" in payloads:
108
111
  del payloads["tools"]
109
112
 
110
113
  completion = await self.client.chat.completions.create(
@@ -137,6 +140,12 @@ class ProviderOpenAIOfficial(Provider):
137
140
 
138
141
  # 不在默认参数中的参数放在 extra_body 中
139
142
  extra_body = {}
143
+
144
+ # 读取并合并 custom_extra_body 配置
145
+ custom_extra_body = self.provider_config.get("custom_extra_body", {})
146
+ if isinstance(custom_extra_body, dict):
147
+ extra_body.update(custom_extra_body)
148
+
140
149
  to_del = []
141
150
  for key in payloads.keys():
142
151
  if key not in self.default_params:
@@ -1,4 +1,5 @@
1
1
  import aiohttp
2
+ from astrbot import logger
2
3
  from ..provider import RerankProvider
3
4
  from ..register import register_provider_adapter
4
5
  from ..entities import ProviderType, RerankResult
@@ -44,6 +45,11 @@ class VLLMRerankProvider(RerankProvider):
44
45
  response_data = await response.json()
45
46
  results = response_data.get("results", [])
46
47
 
48
+ if not results:
49
+ logger.warning(
50
+ f"Rerank API 返回了空的列表数据。原始响应: {response_data}"
51
+ )
52
+
47
53
  return [
48
54
  RerankResult(
49
55
  index=result["index"],
@@ -27,14 +27,16 @@ class Star(CommandParserMixin):
27
27
  star_map[cls.__module__].star_cls_type = cls
28
28
  star_map[cls.__module__].module_path = cls.__module__
29
29
 
30
- @staticmethod
31
- async def text_to_image(text: str, return_url=True) -> str:
30
+ async def text_to_image(self, text: str, return_url=True) -> str:
32
31
  """将文本转换为图片"""
33
- return await html_renderer.render_t2i(text, return_url=return_url)
32
+ return await html_renderer.render_t2i(
33
+ text,
34
+ return_url=return_url,
35
+ template_name=self.context._config.get("t2i_active_template"),
36
+ )
34
37
 
35
- @staticmethod
36
38
  async def html_render(
37
- tmpl: str, data: dict, return_url=True, options: dict | None = None
39
+ self, tmpl: str, data: dict, return_url=True, options: dict | None = None
38
40
  ) -> str:
39
41
  """渲染 HTML"""
40
42
  return await html_renderer.render_custom_template(
@@ -7,7 +7,6 @@ from astrbot.core.config import AstrBotConfig
7
7
  from .custom_filter import CustomFilter
8
8
  from ..star_handler import StarHandlerMetadata
9
9
 
10
-
11
10
  class GreedyStr(str):
12
11
  """标记指令完成其他参数接收后的所有剩余文本。"""
13
12
 
@@ -153,10 +152,17 @@ class CommandFilter(HandlerFilter):
153
152
  _full = f"{parent_command_name} {candidate}"
154
153
  else:
155
154
  _full = candidate
156
- if message_str.startswith(f"{_full} ") or message_str == _full:
157
- message_str = message_str[len(_full) :].strip()
155
+ if message_str == _full:
156
+ # 完全等于命令名 没参数
157
+ message_str = ""
158
+ ok = True
159
+ break
160
+ elif message_str.startswith(_full):
161
+ # 命令名后面无论是空格还是直接连参数都可以
162
+ message_str = message_str[len(_full):].lstrip()
158
163
  ok = True
159
164
  break
165
+
160
166
  if not ok:
161
167
  return False
162
168
 
@@ -18,6 +18,7 @@ class PlatformAdapterType(enum.Flag):
18
18
  KOOK = enum.auto()
19
19
  VOCECHAT = enum.auto()
20
20
  WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
21
+ SATORI = enum.auto()
21
22
  ALL = (
22
23
  AIOCQHTTP
23
24
  | QQOFFICIAL
@@ -31,6 +32,7 @@ class PlatformAdapterType(enum.Flag):
31
32
  | KOOK
32
33
  | VOCECHAT
33
34
  | WEIXIN_OFFICIAL_ACCOUNT
35
+ | SATORI
34
36
  )
35
37
 
36
38
 
@@ -47,6 +49,7 @@ ADAPTER_NAME_2_TYPE = {
47
49
  "wechatpadpro": PlatformAdapterType.WECHATPADPRO,
48
50
  "vocechat": PlatformAdapterType.VOCECHAT,
49
51
  "weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
52
+ "satori": PlatformAdapterType.SATORI,
50
53
  }
51
54
 
52
55
 
@@ -8,6 +8,7 @@ from .star_handler import (
8
8
  register_permission_type,
9
9
  register_custom_filter,
10
10
  register_on_astrbot_loaded,
11
+ register_on_platform_loaded,
11
12
  register_on_llm_request,
12
13
  register_on_llm_response,
13
14
  register_llm_tool,
@@ -26,6 +27,7 @@ __all__ = [
26
27
  "register_permission_type",
27
28
  "register_custom_filter",
28
29
  "register_on_astrbot_loaded",
30
+ "register_on_platform_loaded",
29
31
  "register_on_llm_request",
30
32
  "register_on_llm_response",
31
33
  "register_llm_tool",
@@ -267,6 +267,18 @@ def register_on_astrbot_loaded(**kwargs):
267
267
  return decorator
268
268
 
269
269
 
270
+ def register_on_platform_loaded(**kwargs):
271
+ """
272
+ 当平台加载完成时
273
+ """
274
+
275
+ def decorator(awaitable):
276
+ _ = get_handler_or_create(awaitable, EventType.OnPlatformLoadedEvent, **kwargs)
277
+ return awaitable
278
+
279
+ return decorator
280
+
281
+
270
282
  def register_on_llm_request(**kwargs):
271
283
  """当有 LLM 请求时的事件
272
284
 
@@ -376,9 +388,11 @@ def register_llm_tool(name: str = None, **kwargs):
376
388
  # print(f"Registering tool {llm_tool_name} for agent", registering_agent._agent.name)
377
389
  if registering_agent._agent.tools is None:
378
390
  registering_agent._agent.tools = []
379
- registering_agent._agent.tools.append(llm_tools.spec_to_func(
380
- llm_tool_name, args, docstring.description.strip(), awaitable
381
- ))
391
+ registering_agent._agent.tools.append(
392
+ llm_tools.spec_to_func(
393
+ llm_tool_name, args, docstring.description.strip(), awaitable
394
+ )
395
+ )
382
396
 
383
397
  return awaitable
384
398
 
@@ -421,7 +435,7 @@ def register_agent(
421
435
  run_hooks=run_hooks or BaseAgentRunHooks[AstrAgentContext](),
422
436
  )
423
437
  handoff_tool = HandoffTool(agent=agent)
424
- handoff_tool.handler=awaitable
438
+ handoff_tool.handler = awaitable
425
439
  llm_tools.func_list.append(handoff_tool)
426
440
  return RegisteringAgent(agent)
427
441
 
@@ -34,19 +34,26 @@ class StarHandlerRegistry(Generic[T]):
34
34
  ) -> List[StarHandlerMetadata]:
35
35
  handlers = []
36
36
  for handler in self._handlers:
37
+ # 过滤事件类型
37
38
  if handler.event_type != event_type:
38
39
  continue
40
+ # 过滤启用状态
39
41
  if only_activated:
40
42
  plugin = star_map.get(handler.handler_module_path)
41
43
  if not (plugin and plugin.activated):
42
44
  continue
45
+ # 过滤插件白名单
43
46
  if plugins_name is not None and plugins_name != ["*"]:
44
47
  plugin = star_map.get(handler.handler_module_path)
45
48
  if not plugin:
46
49
  continue
47
50
  if (
48
51
  plugin.name not in plugins_name
49
- and event_type != EventType.OnAstrBotLoadedEvent
52
+ and event_type
53
+ not in (
54
+ EventType.OnAstrBotLoadedEvent,
55
+ EventType.OnPlatformLoadedEvent,
56
+ )
50
57
  and not plugin.reserved
51
58
  ):
52
59
  continue
@@ -90,6 +97,7 @@ class EventType(enum.Enum):
90
97
  """
91
98
 
92
99
  OnAstrBotLoadedEvent = enum.auto() # AstrBot 加载完成
100
+ OnPlatformLoadedEvent = enum.auto() # 平台加载完成
93
101
 
94
102
  AdapterMessageEvent = enum.auto() # 收到适配器发来的消息
95
103
  OnLLMRequestEvent = enum.auto() # 收到 LLM 请求(可以是用户也可以是插件)