AstrBot 4.10.3__py3-none-any.whl → 4.10.4__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 (42) hide show
  1. astrbot/builtin_stars/astrbot/main.py +2 -10
  2. astrbot/builtin_stars/python_interpreter/main.py +130 -131
  3. astrbot/cli/__init__.py +1 -1
  4. astrbot/core/agent/message.py +23 -1
  5. astrbot/core/agent/runners/tool_loop_agent_runner.py +24 -7
  6. astrbot/core/astr_agent_hooks.py +6 -0
  7. astrbot/core/backup/exporter.py +1 -0
  8. astrbot/core/config/astrbot_config.py +2 -0
  9. astrbot/core/config/default.py +47 -6
  10. astrbot/core/knowledge_base/chunking/recursive.py +10 -2
  11. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +184 -174
  12. astrbot/core/pipeline/result_decorate/stage.py +65 -57
  13. astrbot/core/pipeline/waking_check/stage.py +29 -2
  14. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +15 -29
  15. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -6
  16. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +15 -1
  17. astrbot/core/platform/sources/lark/lark_adapter.py +2 -10
  18. astrbot/core/platform/sources/misskey/misskey_adapter.py +0 -5
  19. astrbot/core/platform/sources/misskey/misskey_utils.py +0 -3
  20. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +4 -9
  21. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +4 -9
  22. astrbot/core/platform/sources/satori/satori_adapter.py +6 -1
  23. astrbot/core/platform/sources/slack/slack_adapter.py +3 -6
  24. astrbot/core/platform/sources/webchat/webchat_adapter.py +0 -1
  25. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +3 -5
  26. astrbot/core/provider/entities.py +9 -1
  27. astrbot/core/provider/sources/anthropic_source.py +60 -3
  28. astrbot/core/provider/sources/gemini_source.py +37 -3
  29. astrbot/core/provider/sources/minimax_tts_api_source.py +4 -1
  30. astrbot/core/provider/sources/openai_source.py +25 -31
  31. astrbot/core/provider/sources/xai_source.py +29 -0
  32. astrbot/core/provider/sources/xinference_stt_provider.py +24 -12
  33. astrbot/core/star/star_manager.py +41 -0
  34. astrbot/core/utils/pip_installer.py +20 -1
  35. astrbot/dashboard/routes/backup.py +519 -15
  36. astrbot/dashboard/routes/config.py +45 -0
  37. astrbot/dashboard/server.py +1 -0
  38. {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/METADATA +1 -1
  39. {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/RECORD +42 -41
  40. {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/WHEEL +0 -0
  41. {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/entry_points.txt +0 -0
  42. {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/licenses/LICENSE +0 -0
@@ -74,28 +74,6 @@ class ProviderOpenAIOfficial(Provider):
74
74
 
75
75
  self.reasoning_key = "reasoning_content"
76
76
 
77
- def _maybe_inject_xai_search(self, payloads: dict, **kwargs):
78
- """当开启 xAI 原生搜索时,向请求体注入 Live Search 参数。
79
-
80
- - 仅在 provider_config.xai_native_search 为 True 时生效
81
- - 默认注入 {"mode": "auto"}
82
- - 允许通过 kwargs 使用 xai_search_mode 覆盖(on/auto/off)
83
- """
84
- if not bool(self.provider_config.get("xai_native_search", False)):
85
- return
86
-
87
- mode = kwargs.get("xai_search_mode", "auto")
88
- mode = str(mode).lower()
89
- if mode not in ("auto", "on", "off"):
90
- mode = "auto"
91
-
92
- # off 时不注入,保持与未开启一致
93
- if mode == "off":
94
- return
95
-
96
- # OpenAI SDK 不识别的字段会在 _query/_query_stream 中放入 extra_body
97
- payloads["search_parameters"] = {"mode": mode}
98
-
99
77
  async def get_models(self):
100
78
  try:
101
79
  models_str = []
@@ -134,10 +112,6 @@ class ProviderOpenAIOfficial(Provider):
134
112
 
135
113
  model = payloads.get("model", "").lower()
136
114
 
137
- # 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
138
- if model == "deepseek-reasoner" and "tools" in payloads:
139
- del payloads["tools"]
140
-
141
115
  completion = await self.client.chat.completions.create(
142
116
  **payloads,
143
117
  stream=False,
@@ -251,10 +225,14 @@ class ProviderOpenAIOfficial(Provider):
251
225
  def _extract_usage(self, usage: CompletionUsage) -> TokenUsage:
252
226
  ptd = usage.prompt_tokens_details
253
227
  cached = ptd.cached_tokens if ptd and ptd.cached_tokens else 0
228
+ prompt_tokens = 0 if usage.prompt_tokens is None else usage.prompt_tokens
229
+ completion_tokens = (
230
+ 0 if usage.completion_tokens is None else usage.completion_tokens
231
+ )
254
232
  return TokenUsage(
255
- input_other=usage.prompt_tokens - cached,
256
- input_cached=ptd.cached_tokens if ptd and ptd.cached_tokens else 0,
257
- output=usage.completion_tokens,
233
+ input_other=prompt_tokens - cached,
234
+ input_cached=cached,
235
+ output=completion_tokens,
258
236
  )
259
237
 
260
238
  async def _parse_openai_completion(
@@ -381,11 +359,27 @@ class ProviderOpenAIOfficial(Provider):
381
359
 
382
360
  payloads = {"messages": context_query, "model": model}
383
361
 
384
- # xAI origin search tool inject
385
- self._maybe_inject_xai_search(payloads, **kwargs)
362
+ self._finally_convert_payload(payloads)
386
363
 
387
364
  return payloads, context_query
388
365
 
366
+ def _finally_convert_payload(self, payloads: dict):
367
+ """Finally convert the payload. Such as think part conversion, tool inject."""
368
+ for message in payloads.get("messages", []):
369
+ if message.get("role") == "assistant" and isinstance(
370
+ message.get("content"), list
371
+ ):
372
+ reasoning_content = ""
373
+ new_content = [] # not including think part
374
+ for part in message["content"]:
375
+ if part.get("type") == "think":
376
+ reasoning_content += str(part.get("think"))
377
+ else:
378
+ new_content.append(part)
379
+ message["content"] = new_content
380
+ # reasoning key is "reasoning_content"
381
+ message["reasoning_content"] = reasoning_content
382
+
389
383
  async def _handle_api_error(
390
384
  self,
391
385
  e: Exception,
@@ -0,0 +1,29 @@
1
+ from ..register import register_provider_adapter
2
+ from .openai_source import ProviderOpenAIOfficial
3
+
4
+
5
+ @register_provider_adapter(
6
+ "xai_chat_completion", "xAI Chat Completion Provider Adapter"
7
+ )
8
+ class ProviderXAI(ProviderOpenAIOfficial):
9
+ def __init__(
10
+ self,
11
+ provider_config: dict,
12
+ provider_settings: dict,
13
+ ) -> None:
14
+ super().__init__(provider_config, provider_settings)
15
+
16
+ def _maybe_inject_xai_search(self, payloads: dict):
17
+ """当开启 xAI 原生搜索时,向请求体注入 Live Search 参数。
18
+
19
+ - 仅在 provider_config.xai_native_search 为 True 时生效
20
+ - 默认注入 {"mode": "auto"}
21
+ """
22
+ if not bool(self.provider_config.get("xai_native_search", False)):
23
+ return
24
+ # OpenAI SDK 不识别的字段会在 _query/_query_stream 中放入 extra_body
25
+ payloads["search_parameters"] = {"mode": "auto"}
26
+
27
+ def _finally_convert_payload(self, payloads: dict):
28
+ self._maybe_inject_xai_search(payloads)
29
+ super()._finally_convert_payload(payloads)
@@ -8,7 +8,10 @@ from xinference_client.client.restful.async_restful_client import (
8
8
 
9
9
  from astrbot.core import logger
10
10
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
11
- from astrbot.core.utils.tencent_record_helper import tencent_silk_to_wav
11
+ from astrbot.core.utils.tencent_record_helper import (
12
+ convert_to_pcm_wav,
13
+ tencent_silk_to_wav,
14
+ )
12
15
 
13
16
  from ..entities import ProviderType
14
17
  from ..provider import STTProvider
@@ -111,17 +114,22 @@ class ProviderXinferenceSTT(STTProvider):
111
114
  return ""
112
115
 
113
116
  # 2. Check for conversion
114
- needs_conversion = False
115
- if (
116
- audio_url.endswith((".amr", ".silk"))
117
- or is_tencent
118
- or b"SILK" in audio_bytes[:8]
119
- ):
120
- needs_conversion = True
117
+ conversion_type = None
118
+
119
+ if b"SILK" in audio_bytes[:8]:
120
+ conversion_type = "silk"
121
+ elif b"#!AMR" in audio_bytes[:6]:
122
+ conversion_type = "amr"
123
+ elif audio_url.endswith(".silk") or is_tencent:
124
+ conversion_type = "silk"
125
+ elif audio_url.endswith(".amr"):
126
+ conversion_type = "amr"
121
127
 
122
128
  # 3. Perform conversion if needed
123
- if needs_conversion:
124
- logger.info("Audio requires conversion, using temporary files...")
129
+ if conversion_type:
130
+ logger.info(
131
+ f"Audio requires conversion ({conversion_type}), using temporary files..."
132
+ )
125
133
  temp_dir = os.path.join(get_astrbot_data_path(), "temp")
126
134
  os.makedirs(temp_dir, exist_ok=True)
127
135
 
@@ -132,8 +140,12 @@ class ProviderXinferenceSTT(STTProvider):
132
140
  with open(input_path, "wb") as f:
133
141
  f.write(audio_bytes)
134
142
 
135
- logger.info("Converting silk/amr file to wav ...")
136
- await tencent_silk_to_wav(input_path, output_path)
143
+ if conversion_type == "silk":
144
+ logger.info("Converting silk to wav ...")
145
+ await tencent_silk_to_wav(input_path, output_path)
146
+ elif conversion_type == "amr":
147
+ logger.info("Converting amr to wav ...")
148
+ await convert_to_pcm_wav(input_path, output_path)
137
149
 
138
150
  with open(output_path, "rb") as f:
139
151
  audio_bytes = f.read()
@@ -944,8 +944,49 @@ class PluginManager:
944
944
  dir_name = os.path.basename(zip_file_path).replace(".zip", "")
945
945
  dir_name = dir_name.removesuffix("-master").removesuffix("-main").lower()
946
946
  desti_dir = os.path.join(self.plugin_store_path, dir_name)
947
+
948
+ # 第一步:检查是否已安装同目录名的插件,先终止旧插件
949
+ existing_plugin = None
950
+ for star in self.context.get_all_stars():
951
+ if star.root_dir_name == dir_name:
952
+ existing_plugin = star
953
+ break
954
+
955
+ if existing_plugin:
956
+ logger.info(f"检测到插件 {existing_plugin.name} 已安装,正在终止旧插件...")
957
+ try:
958
+ await self._terminate_plugin(existing_plugin)
959
+ except Exception:
960
+ logger.warning(traceback.format_exc())
961
+ if existing_plugin.name and existing_plugin.module_path:
962
+ await self._unbind_plugin(
963
+ existing_plugin.name, existing_plugin.module_path
964
+ )
965
+
947
966
  self.updator.unzip_file(zip_file_path, desti_dir)
948
967
 
968
+ # 第二步:解压后,读取新插件的 metadata.yaml,检查是否存在同名但不同目录的插件
969
+ try:
970
+ new_metadata = self._load_plugin_metadata(desti_dir)
971
+ if new_metadata and new_metadata.name:
972
+ for star in self.context.get_all_stars():
973
+ if (
974
+ star.name == new_metadata.name
975
+ and star.root_dir_name != dir_name
976
+ ):
977
+ logger.warning(
978
+ f"检测到同名插件 {star.name} 存在于不同目录 {star.root_dir_name},正在终止..."
979
+ )
980
+ try:
981
+ await self._terminate_plugin(star)
982
+ except Exception:
983
+ logger.warning(traceback.format_exc())
984
+ if star.name and star.module_path:
985
+ await self._unbind_plugin(star.name, star.module_path)
986
+ break # 只处理第一个匹配的
987
+ except Exception as e:
988
+ logger.debug(f"读取新插件 metadata.yaml 失败,跳过同名检查: {e!s}")
989
+
949
990
  # remove the zip
950
991
  try:
951
992
  os.remove(zip_file_path)
@@ -1,10 +1,29 @@
1
1
  import asyncio
2
+ import locale
2
3
  import logging
3
4
  import sys
4
5
 
5
6
  logger = logging.getLogger("astrbot")
6
7
 
7
8
 
9
+ def _robust_decode(line: bytes) -> str:
10
+ """解码字节流,兼容不同平台的编码"""
11
+ try:
12
+ return line.decode("utf-8").strip()
13
+ except UnicodeDecodeError:
14
+ pass
15
+ try:
16
+ return line.decode(locale.getpreferredencoding(False)).strip()
17
+ except UnicodeDecodeError:
18
+ pass
19
+ if sys.platform.startswith("win"):
20
+ try:
21
+ return line.decode("gbk").strip()
22
+ except UnicodeDecodeError:
23
+ pass
24
+ return line.decode("utf-8", errors="replace").strip()
25
+
26
+
8
27
  class PipInstaller:
9
28
  def __init__(self, pip_install_arg: str, pypi_index_url: str | None = None):
10
29
  self.pip_install_arg = pip_install_arg
@@ -42,7 +61,7 @@ class PipInstaller:
42
61
 
43
62
  assert process.stdout is not None
44
63
  async for line in process.stdout:
45
- logger.info(line.decode().strip())
64
+ logger.info(_robust_decode(line))
46
65
 
47
66
  await process.wait()
48
67