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.
- astrbot/builtin_stars/astrbot/main.py +2 -10
- astrbot/builtin_stars/python_interpreter/main.py +130 -131
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/message.py +23 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +24 -7
- astrbot/core/astr_agent_hooks.py +6 -0
- astrbot/core/backup/exporter.py +1 -0
- astrbot/core/config/astrbot_config.py +2 -0
- astrbot/core/config/default.py +47 -6
- astrbot/core/knowledge_base/chunking/recursive.py +10 -2
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +184 -174
- astrbot/core/pipeline/result_decorate/stage.py +65 -57
- astrbot/core/pipeline/waking_check/stage.py +29 -2
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +15 -29
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -6
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +15 -1
- astrbot/core/platform/sources/lark/lark_adapter.py +2 -10
- astrbot/core/platform/sources/misskey/misskey_adapter.py +0 -5
- astrbot/core/platform/sources/misskey/misskey_utils.py +0 -3
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +4 -9
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +4 -9
- astrbot/core/platform/sources/satori/satori_adapter.py +6 -1
- astrbot/core/platform/sources/slack/slack_adapter.py +3 -6
- astrbot/core/platform/sources/webchat/webchat_adapter.py +0 -1
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +3 -5
- astrbot/core/provider/entities.py +9 -1
- astrbot/core/provider/sources/anthropic_source.py +60 -3
- astrbot/core/provider/sources/gemini_source.py +37 -3
- astrbot/core/provider/sources/minimax_tts_api_source.py +4 -1
- astrbot/core/provider/sources/openai_source.py +25 -31
- astrbot/core/provider/sources/xai_source.py +29 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +24 -12
- astrbot/core/star/star_manager.py +41 -0
- astrbot/core/utils/pip_installer.py +20 -1
- astrbot/dashboard/routes/backup.py +519 -15
- astrbot/dashboard/routes/config.py +45 -0
- astrbot/dashboard/server.py +1 -0
- {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/METADATA +1 -1
- {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/RECORD +42 -41
- {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/WHEEL +0 -0
- {astrbot-4.10.3.dist-info → astrbot-4.10.4.dist-info}/entry_points.txt +0 -0
- {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=
|
|
256
|
-
input_cached=
|
|
257
|
-
output=
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
124
|
-
logger.info(
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
64
|
+
logger.info(_robust_decode(line))
|
|
46
65
|
|
|
47
66
|
await process.wait()
|
|
48
67
|
|