AstrBot 4.10.2__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/long_term_memory.py +186 -0
- astrbot/builtin_stars/astrbot/main.py +120 -0
- astrbot/builtin_stars/astrbot/metadata.yaml +4 -0
- astrbot/builtin_stars/astrbot/process_llm_request.py +245 -0
- astrbot/builtin_stars/builtin_commands/commands/__init__.py +31 -0
- astrbot/builtin_stars/builtin_commands/commands/admin.py +77 -0
- astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +173 -0
- astrbot/builtin_stars/builtin_commands/commands/conversation.py +366 -0
- astrbot/builtin_stars/builtin_commands/commands/help.py +88 -0
- astrbot/builtin_stars/builtin_commands/commands/llm.py +20 -0
- astrbot/builtin_stars/builtin_commands/commands/persona.py +142 -0
- astrbot/builtin_stars/builtin_commands/commands/plugin.py +120 -0
- astrbot/builtin_stars/builtin_commands/commands/provider.py +329 -0
- astrbot/builtin_stars/builtin_commands/commands/setunset.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/sid.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/t2i.py +23 -0
- astrbot/builtin_stars/builtin_commands/commands/tool.py +31 -0
- astrbot/builtin_stars/builtin_commands/commands/tts.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/utils/rst_scene.py +26 -0
- astrbot/builtin_stars/builtin_commands/main.py +237 -0
- astrbot/builtin_stars/builtin_commands/metadata.yaml +4 -0
- astrbot/builtin_stars/python_interpreter/main.py +536 -0
- astrbot/builtin_stars/python_interpreter/metadata.yaml +4 -0
- astrbot/builtin_stars/python_interpreter/requirements.txt +1 -0
- astrbot/builtin_stars/python_interpreter/shared/api.py +22 -0
- astrbot/builtin_stars/reminder/main.py +266 -0
- astrbot/builtin_stars/reminder/metadata.yaml +4 -0
- astrbot/builtin_stars/session_controller/main.py +114 -0
- astrbot/builtin_stars/session_controller/metadata.yaml +5 -0
- astrbot/builtin_stars/web_searcher/engines/__init__.py +111 -0
- astrbot/builtin_stars/web_searcher/engines/bing.py +30 -0
- astrbot/builtin_stars/web_searcher/engines/sogo.py +52 -0
- astrbot/builtin_stars/web_searcher/main.py +436 -0
- astrbot/builtin_stars/web_searcher/metadata.yaml +4 -0
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/message.py +32 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +26 -8
- astrbot/core/astr_agent_hooks.py +6 -0
- astrbot/core/backup/__init__.py +26 -0
- astrbot/core/backup/constants.py +77 -0
- astrbot/core/backup/exporter.py +477 -0
- astrbot/core/backup/importer.py +761 -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/log.py +1 -1
- 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 +31 -3
- 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 +41 -10
- astrbot/core/provider/provider.py +3 -1
- astrbot/core/provider/sources/anthropic_source.py +140 -30
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +14 -6
- astrbot/core/provider/sources/gemini_source.py +112 -29
- astrbot/core/provider/sources/minimax_tts_api_source.py +4 -1
- astrbot/core/provider/sources/openai_source.py +93 -56
- astrbot/core/provider/sources/xai_source.py +29 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +24 -12
- astrbot/core/star/context.py +1 -1
- astrbot/core/star/star_manager.py +52 -13
- astrbot/core/utils/astrbot_path.py +34 -0
- astrbot/core/utils/pip_installer.py +20 -1
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/backup.py +1093 -0
- astrbot/dashboard/routes/config.py +45 -0
- astrbot/dashboard/routes/log.py +44 -10
- astrbot/dashboard/server.py +9 -1
- {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/METADATA +1 -1
- {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/RECORD +84 -44
- {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/WHEEL +0 -0
- {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/entry_points.txt +0 -0
- {astrbot-4.10.2.dist-info → astrbot-4.10.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,6 +13,7 @@ from google.genai.errors import APIError
|
|
|
13
13
|
import astrbot.core.message.components as Comp
|
|
14
14
|
from astrbot import logger
|
|
15
15
|
from astrbot.api.provider import Provider
|
|
16
|
+
from astrbot.core.agent.message import ContentPart, ImageURLPart, TextPart
|
|
16
17
|
from astrbot.core.message.message_event_result import MessageChain
|
|
17
18
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
|
18
19
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
|
@@ -320,9 +321,37 @@ class ProviderGoogleGenAI(Provider):
|
|
|
320
321
|
append_or_extend(gemini_contents, parts, types.UserContent)
|
|
321
322
|
|
|
322
323
|
elif role == "assistant":
|
|
323
|
-
if content:
|
|
324
|
+
if isinstance(content, str):
|
|
324
325
|
parts = [types.Part.from_text(text=content)]
|
|
325
326
|
append_or_extend(gemini_contents, parts, types.ModelContent)
|
|
327
|
+
elif isinstance(content, list):
|
|
328
|
+
parts = []
|
|
329
|
+
thinking_signature = None
|
|
330
|
+
text = ""
|
|
331
|
+
for part in content:
|
|
332
|
+
# for most cases, assistant content only contains two parts: think and text
|
|
333
|
+
if part.get("type") == "think":
|
|
334
|
+
thinking_signature = part.get("encrypted") or None
|
|
335
|
+
else:
|
|
336
|
+
text += str(part.get("text"))
|
|
337
|
+
|
|
338
|
+
if thinking_signature and isinstance(thinking_signature, str):
|
|
339
|
+
try:
|
|
340
|
+
thinking_signature = base64.b64decode(thinking_signature)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.warning(
|
|
343
|
+
f"Failed to decode google gemini thinking signature: {e}",
|
|
344
|
+
exc_info=True,
|
|
345
|
+
)
|
|
346
|
+
thinking_signature = None
|
|
347
|
+
parts.append(
|
|
348
|
+
types.Part(
|
|
349
|
+
text=text,
|
|
350
|
+
thought_signature=thinking_signature,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
append_or_extend(gemini_contents, parts, types.ModelContent)
|
|
354
|
+
|
|
326
355
|
elif not native_tool_enabled and "tool_calls" in message:
|
|
327
356
|
parts = []
|
|
328
357
|
for tool in message["tool_calls"]:
|
|
@@ -440,7 +469,8 @@ class ProviderGoogleGenAI(Provider):
|
|
|
440
469
|
for part in result_parts:
|
|
441
470
|
if part.text:
|
|
442
471
|
chain.append(Comp.Plain(part.text))
|
|
443
|
-
|
|
472
|
+
|
|
473
|
+
if (
|
|
444
474
|
part.function_call
|
|
445
475
|
and part.function_call.name is not None
|
|
446
476
|
and part.function_call.args is not None
|
|
@@ -457,13 +487,18 @@ class ProviderGoogleGenAI(Provider):
|
|
|
457
487
|
llm_response.tools_call_extra_content[tool_call_id] = {
|
|
458
488
|
"google": {"thought_signature": ts_bs64}
|
|
459
489
|
}
|
|
460
|
-
|
|
490
|
+
|
|
491
|
+
if (
|
|
461
492
|
part.inline_data
|
|
462
493
|
and part.inline_data.mime_type
|
|
463
494
|
and part.inline_data.mime_type.startswith("image/")
|
|
464
495
|
and part.inline_data.data
|
|
465
496
|
):
|
|
466
497
|
chain.append(Comp.Image.fromBytes(part.inline_data.data))
|
|
498
|
+
|
|
499
|
+
if ts := part.thought_signature:
|
|
500
|
+
# only keep the last thinking signature
|
|
501
|
+
llm_response.reasoning_signature = base64.b64encode(ts).decode("utf-8")
|
|
467
502
|
return MessageChain(chain=chain)
|
|
468
503
|
|
|
469
504
|
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
|
|
@@ -680,13 +715,16 @@ class ProviderGoogleGenAI(Provider):
|
|
|
680
715
|
system_prompt=None,
|
|
681
716
|
tool_calls_result=None,
|
|
682
717
|
model=None,
|
|
718
|
+
extra_user_content_parts=None,
|
|
683
719
|
**kwargs,
|
|
684
720
|
) -> LLMResponse:
|
|
685
721
|
if contexts is None:
|
|
686
722
|
contexts = []
|
|
687
723
|
new_record = None
|
|
688
724
|
if prompt is not None:
|
|
689
|
-
new_record = await self.assemble_context(
|
|
725
|
+
new_record = await self.assemble_context(
|
|
726
|
+
prompt, image_urls, extra_user_content_parts
|
|
727
|
+
)
|
|
690
728
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
691
729
|
if new_record:
|
|
692
730
|
context_query.append(new_record)
|
|
@@ -732,13 +770,16 @@ class ProviderGoogleGenAI(Provider):
|
|
|
732
770
|
system_prompt=None,
|
|
733
771
|
tool_calls_result=None,
|
|
734
772
|
model=None,
|
|
773
|
+
extra_user_content_parts=None,
|
|
735
774
|
**kwargs,
|
|
736
775
|
) -> AsyncGenerator[LLMResponse, None]:
|
|
737
776
|
if contexts is None:
|
|
738
777
|
contexts = []
|
|
739
778
|
new_record = None
|
|
740
779
|
if prompt is not None:
|
|
741
|
-
new_record = await self.assemble_context(
|
|
780
|
+
new_record = await self.assemble_context(
|
|
781
|
+
prompt, image_urls, extra_user_content_parts
|
|
782
|
+
)
|
|
742
783
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
743
784
|
if new_record:
|
|
744
785
|
context_query.append(new_record)
|
|
@@ -797,33 +838,75 @@ class ProviderGoogleGenAI(Provider):
|
|
|
797
838
|
self.chosen_api_key = key
|
|
798
839
|
self._init_client()
|
|
799
840
|
|
|
800
|
-
async def assemble_context(
|
|
841
|
+
async def assemble_context(
|
|
842
|
+
self,
|
|
843
|
+
text: str,
|
|
844
|
+
image_urls: list[str] | None = None,
|
|
845
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
846
|
+
):
|
|
801
847
|
"""组装上下文。"""
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
848
|
+
|
|
849
|
+
async def resolve_image_part(image_url: str) -> dict | None:
|
|
850
|
+
if image_url.startswith("http"):
|
|
851
|
+
image_path = await download_image_by_url(image_url)
|
|
852
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
853
|
+
elif image_url.startswith("file:///"):
|
|
854
|
+
image_path = image_url.replace("file:///", "")
|
|
855
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
856
|
+
else:
|
|
857
|
+
image_data = await self.encode_image_bs64(image_url)
|
|
858
|
+
if not image_data:
|
|
859
|
+
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
860
|
+
return None
|
|
861
|
+
return {
|
|
862
|
+
"type": "image_url",
|
|
863
|
+
"image_url": {"url": image_data},
|
|
806
864
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
865
|
+
|
|
866
|
+
# 构建内容块列表
|
|
867
|
+
content_blocks = []
|
|
868
|
+
|
|
869
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
870
|
+
if text:
|
|
871
|
+
content_blocks.append({"type": "text", "text": text})
|
|
872
|
+
elif image_urls:
|
|
873
|
+
# 如果没有文本但有图片,添加占位文本
|
|
874
|
+
content_blocks.append({"type": "text", "text": "[图片]"})
|
|
875
|
+
elif extra_user_content_parts:
|
|
876
|
+
# 如果只有额外内容块,也需要添加占位文本
|
|
877
|
+
content_blocks.append({"type": "text", "text": " "})
|
|
878
|
+
|
|
879
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
880
|
+
if extra_user_content_parts:
|
|
881
|
+
for part in extra_user_content_parts:
|
|
882
|
+
if isinstance(part, TextPart):
|
|
883
|
+
content_blocks.append({"type": "text", "text": part.text})
|
|
884
|
+
elif isinstance(part, ImageURLPart):
|
|
885
|
+
image_part = await resolve_image_part(part.image_url.url)
|
|
886
|
+
if image_part:
|
|
887
|
+
content_blocks.append(image_part)
|
|
814
888
|
else:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
889
|
+
raise ValueError(f"不支持的额外内容块类型: {type(part)}")
|
|
890
|
+
|
|
891
|
+
# 3. 图片内容
|
|
892
|
+
if image_urls:
|
|
893
|
+
for image_url in image_urls:
|
|
894
|
+
image_part = await resolve_image_part(image_url)
|
|
895
|
+
if image_part:
|
|
896
|
+
content_blocks.append(image_part)
|
|
897
|
+
|
|
898
|
+
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
|
899
|
+
if (
|
|
900
|
+
text
|
|
901
|
+
and not extra_user_content_parts
|
|
902
|
+
and not image_urls
|
|
903
|
+
and len(content_blocks) == 1
|
|
904
|
+
and content_blocks[0]["type"] == "text"
|
|
905
|
+
):
|
|
906
|
+
return {"role": "user", "content": content_blocks[0]["text"]}
|
|
907
|
+
|
|
908
|
+
# 否则返回多模态格式
|
|
909
|
+
return {"role": "user", "content": content_blocks}
|
|
827
910
|
|
|
828
911
|
async def encode_image_bs64(self, image_url: str) -> str:
|
|
829
912
|
"""将图片转换为 base64"""
|
|
@@ -51,7 +51,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
|
|
|
51
51
|
"voice_id": ""
|
|
52
52
|
if self.is_timber_weight
|
|
53
53
|
else provider_config.get("minimax-voice-id", ""),
|
|
54
|
-
"emotion": provider_config.get("minimax-voice-emotion", "
|
|
54
|
+
"emotion": provider_config.get("minimax-voice-emotion", "auto"),
|
|
55
55
|
"latex_read": provider_config.get("minimax-voice-latex", False),
|
|
56
56
|
"english_normalization": provider_config.get(
|
|
57
57
|
"minimax-voice-english-normalization",
|
|
@@ -59,6 +59,9 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
|
|
|
59
59
|
),
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
if self.voice_setting["emotion"] == "auto":
|
|
63
|
+
self.voice_setting.pop("emotion", None)
|
|
64
|
+
|
|
62
65
|
self.audio_setting: dict = {
|
|
63
66
|
"sample_rate": 32000,
|
|
64
67
|
"bitrate": 128000,
|
|
@@ -17,7 +17,7 @@ from openai.types.completion_usage import CompletionUsage
|
|
|
17
17
|
import astrbot.core.message.components as Comp
|
|
18
18
|
from astrbot import logger
|
|
19
19
|
from astrbot.api.provider import Provider
|
|
20
|
-
from astrbot.core.agent.message import Message
|
|
20
|
+
from astrbot.core.agent.message import ContentPart, ImageURLPart, Message, TextPart
|
|
21
21
|
from astrbot.core.agent.tool import ToolSet
|
|
22
22
|
from astrbot.core.message.message_event_result import MessageChain
|
|
23
23
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult
|
|
@@ -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(
|
|
@@ -348,6 +326,7 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
348
326
|
system_prompt: str | None = None,
|
|
349
327
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
|
350
328
|
model: str | None = None,
|
|
329
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
351
330
|
**kwargs,
|
|
352
331
|
) -> tuple:
|
|
353
332
|
"""准备聊天所需的有效载荷和上下文"""
|
|
@@ -355,7 +334,9 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
355
334
|
contexts = []
|
|
356
335
|
new_record = None
|
|
357
336
|
if prompt is not None:
|
|
358
|
-
new_record = await self.assemble_context(
|
|
337
|
+
new_record = await self.assemble_context(
|
|
338
|
+
prompt, image_urls, extra_user_content_parts
|
|
339
|
+
)
|
|
359
340
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
360
341
|
if new_record:
|
|
361
342
|
context_query.append(new_record)
|
|
@@ -378,11 +359,27 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
378
359
|
|
|
379
360
|
payloads = {"messages": context_query, "model": model}
|
|
380
361
|
|
|
381
|
-
|
|
382
|
-
self._maybe_inject_xai_search(payloads, **kwargs)
|
|
362
|
+
self._finally_convert_payload(payloads)
|
|
383
363
|
|
|
384
364
|
return payloads, context_query
|
|
385
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
|
+
|
|
386
383
|
async def _handle_api_error(
|
|
387
384
|
self,
|
|
388
385
|
e: Exception,
|
|
@@ -476,6 +473,7 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
476
473
|
system_prompt=None,
|
|
477
474
|
tool_calls_result=None,
|
|
478
475
|
model=None,
|
|
476
|
+
extra_user_content_parts=None,
|
|
479
477
|
**kwargs,
|
|
480
478
|
) -> LLMResponse:
|
|
481
479
|
payloads, context_query = await self._prepare_chat_payload(
|
|
@@ -485,6 +483,7 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
485
483
|
system_prompt,
|
|
486
484
|
tool_calls_result,
|
|
487
485
|
model=model,
|
|
486
|
+
extra_user_content_parts=extra_user_content_parts,
|
|
488
487
|
**kwargs,
|
|
489
488
|
)
|
|
490
489
|
|
|
@@ -624,33 +623,71 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
624
623
|
self,
|
|
625
624
|
text: str,
|
|
626
625
|
image_urls: list[str] | None = None,
|
|
626
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
627
627
|
) -> dict:
|
|
628
628
|
"""组装成符合 OpenAI 格式的 role 为 user 的消息段"""
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
629
|
+
|
|
630
|
+
async def resolve_image_part(image_url: str) -> dict | None:
|
|
631
|
+
if image_url.startswith("http"):
|
|
632
|
+
image_path = await download_image_by_url(image_url)
|
|
633
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
634
|
+
elif image_url.startswith("file:///"):
|
|
635
|
+
image_path = image_url.replace("file:///", "")
|
|
636
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
637
|
+
else:
|
|
638
|
+
image_data = await self.encode_image_bs64(image_url)
|
|
639
|
+
if not image_data:
|
|
640
|
+
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
641
|
+
return None
|
|
642
|
+
return {
|
|
643
|
+
"type": "image_url",
|
|
644
|
+
"image_url": {"url": image_data},
|
|
633
645
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
646
|
+
|
|
647
|
+
# 构建内容块列表
|
|
648
|
+
content_blocks = []
|
|
649
|
+
|
|
650
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
651
|
+
if text:
|
|
652
|
+
content_blocks.append({"type": "text", "text": text})
|
|
653
|
+
elif image_urls:
|
|
654
|
+
# 如果没有文本但有图片,添加占位文本
|
|
655
|
+
content_blocks.append({"type": "text", "text": "[图片]"})
|
|
656
|
+
elif extra_user_content_parts:
|
|
657
|
+
# 如果只有额外内容块,也需要添加占位文本
|
|
658
|
+
content_blocks.append({"type": "text", "text": " "})
|
|
659
|
+
|
|
660
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
661
|
+
if extra_user_content_parts:
|
|
662
|
+
for part in extra_user_content_parts:
|
|
663
|
+
if isinstance(part, TextPart):
|
|
664
|
+
content_blocks.append({"type": "text", "text": part.text})
|
|
665
|
+
elif isinstance(part, ImageURLPart):
|
|
666
|
+
image_part = await resolve_image_part(part.image_url.url)
|
|
667
|
+
if image_part:
|
|
668
|
+
content_blocks.append(image_part)
|
|
641
669
|
else:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
670
|
+
raise ValueError(f"不支持的额外内容块类型: {type(part)}")
|
|
671
|
+
|
|
672
|
+
# 3. 图片内容
|
|
673
|
+
if image_urls:
|
|
674
|
+
for image_url in image_urls:
|
|
675
|
+
image_part = await resolve_image_part(image_url)
|
|
676
|
+
if image_part:
|
|
677
|
+
content_blocks.append(image_part)
|
|
678
|
+
|
|
679
|
+
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
|
680
|
+
if (
|
|
681
|
+
text
|
|
682
|
+
and not extra_user_content_parts
|
|
683
|
+
and not image_urls
|
|
684
|
+
and len(content_blocks) == 1
|
|
685
|
+
and content_blocks[0]["type"] == "text"
|
|
686
|
+
):
|
|
687
|
+
return {"role": "user", "content": content_blocks[0]["text"]}
|
|
688
|
+
|
|
689
|
+
# 否则返回多模态格式
|
|
690
|
+
return {"role": "user", "content": content_blocks}
|
|
654
691
|
|
|
655
692
|
async def encode_image_bs64(self, image_url: str) -> str:
|
|
656
693
|
"""将图片转换为 base64"""
|
|
@@ -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()
|
astrbot/core/star/context.py
CHANGED
|
@@ -377,7 +377,7 @@ class Context:
|
|
|
377
377
|
if not module_path:
|
|
378
378
|
_parts = []
|
|
379
379
|
module_part = tool.__module__.split(".")
|
|
380
|
-
flags = ["
|
|
380
|
+
flags = ["builtin_stars", "plugins"]
|
|
381
381
|
for i, part in enumerate(module_part):
|
|
382
382
|
_parts.append(part)
|
|
383
383
|
if part in flags and i + 1 < len(module_part):
|