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
|
@@ -14,6 +14,7 @@ import astrbot.core.message.components as Comp
|
|
|
14
14
|
from astrbot import logger
|
|
15
15
|
from astrbot.core.agent.message import (
|
|
16
16
|
AssistantMessageSegment,
|
|
17
|
+
ContentPart,
|
|
17
18
|
ToolCall,
|
|
18
19
|
ToolCallMessageSegment,
|
|
19
20
|
)
|
|
@@ -92,6 +93,8 @@ class ProviderRequest:
|
|
|
92
93
|
"""会话 ID"""
|
|
93
94
|
image_urls: list[str] = field(default_factory=list)
|
|
94
95
|
"""图片 URL 列表"""
|
|
96
|
+
extra_user_content_parts: list[ContentPart] = field(default_factory=list)
|
|
97
|
+
"""额外的用户消息内容部分列表,用于在用户消息后添加额外的内容块(如系统提醒、指令等)。支持 dict 或 ContentPart 对象"""
|
|
95
98
|
func_tool: ToolSet | None = None
|
|
96
99
|
"""可用的函数工具"""
|
|
97
100
|
contexts: list[dict] = field(default_factory=list)
|
|
@@ -166,13 +169,23 @@ class ProviderRequest:
|
|
|
166
169
|
|
|
167
170
|
async def assemble_context(self) -> dict:
|
|
168
171
|
"""将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。"""
|
|
172
|
+
# 构建内容块列表
|
|
173
|
+
content_blocks = []
|
|
174
|
+
|
|
175
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
176
|
+
if self.prompt and self.prompt.strip():
|
|
177
|
+
content_blocks.append({"type": "text", "text": self.prompt})
|
|
178
|
+
elif self.image_urls:
|
|
179
|
+
# 如果没有文本但有图片,添加占位文本
|
|
180
|
+
content_blocks.append({"type": "text", "text": "[图片]"})
|
|
181
|
+
|
|
182
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
183
|
+
if self.extra_user_content_parts:
|
|
184
|
+
for part in self.extra_user_content_parts:
|
|
185
|
+
content_blocks.append(part.model_dump())
|
|
186
|
+
|
|
187
|
+
# 3. 图片内容
|
|
169
188
|
if self.image_urls:
|
|
170
|
-
user_content = {
|
|
171
|
-
"role": "user",
|
|
172
|
-
"content": [
|
|
173
|
-
{"type": "text", "text": self.prompt if self.prompt else "[图片]"},
|
|
174
|
-
],
|
|
175
|
-
}
|
|
176
189
|
for image_url in self.image_urls:
|
|
177
190
|
if image_url.startswith("http"):
|
|
178
191
|
image_path = await download_image_by_url(image_url)
|
|
@@ -185,11 +198,21 @@ class ProviderRequest:
|
|
|
185
198
|
if not image_data:
|
|
186
199
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
187
200
|
continue
|
|
188
|
-
|
|
201
|
+
content_blocks.append(
|
|
189
202
|
{"type": "image_url", "image_url": {"url": image_data}},
|
|
190
203
|
)
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
|
|
205
|
+
# 只有当只有一个来自 prompt 的文本块且没有额外内容块时,才降级为简单格式以保持向后兼容
|
|
206
|
+
if (
|
|
207
|
+
len(content_blocks) == 1
|
|
208
|
+
and content_blocks[0]["type"] == "text"
|
|
209
|
+
and not self.extra_user_content_parts
|
|
210
|
+
and not self.image_urls
|
|
211
|
+
):
|
|
212
|
+
return {"role": "user", "content": content_blocks[0]["text"]}
|
|
213
|
+
|
|
214
|
+
# 否则返回多模态格式
|
|
215
|
+
return {"role": "user", "content": content_blocks}
|
|
193
216
|
|
|
194
217
|
async def _encode_image_bs64(self, image_url: str) -> str:
|
|
195
218
|
"""将图片转换为 base64"""
|
|
@@ -249,6 +272,8 @@ class LLMResponse:
|
|
|
249
272
|
"""Tool call extra content. tool_call_id -> extra_content dict"""
|
|
250
273
|
reasoning_content: str = ""
|
|
251
274
|
"""The reasoning content extracted from the LLM, if any."""
|
|
275
|
+
reasoning_signature: str | None = None
|
|
276
|
+
"""The signature of the reasoning content, if any."""
|
|
252
277
|
|
|
253
278
|
raw_completion: (
|
|
254
279
|
ChatCompletion | GenerateContentResponse | AnthropicMessage | None
|
|
@@ -269,12 +294,14 @@ class LLMResponse:
|
|
|
269
294
|
def __init__(
|
|
270
295
|
self,
|
|
271
296
|
role: str,
|
|
272
|
-
completion_text: str =
|
|
297
|
+
completion_text: str | None = None,
|
|
273
298
|
result_chain: MessageChain | None = None,
|
|
274
299
|
tools_call_args: list[dict[str, Any]] | None = None,
|
|
275
300
|
tools_call_name: list[str] | None = None,
|
|
276
301
|
tools_call_ids: list[str] | None = None,
|
|
277
302
|
tools_call_extra_content: dict[str, dict[str, Any]] | None = None,
|
|
303
|
+
reasoning_content: str | None = None,
|
|
304
|
+
reasoning_signature: str | None = None,
|
|
278
305
|
raw_completion: ChatCompletion
|
|
279
306
|
| GenerateContentResponse
|
|
280
307
|
| AnthropicMessage
|
|
@@ -294,6 +321,8 @@ class LLMResponse:
|
|
|
294
321
|
raw_completion (ChatCompletion, optional): 原始响应, OpenAI 格式. Defaults to None.
|
|
295
322
|
|
|
296
323
|
"""
|
|
324
|
+
if reasoning_content is None:
|
|
325
|
+
reasoning_content = ""
|
|
297
326
|
if tools_call_args is None:
|
|
298
327
|
tools_call_args = []
|
|
299
328
|
if tools_call_name is None:
|
|
@@ -310,6 +339,8 @@ class LLMResponse:
|
|
|
310
339
|
self.tools_call_name = tools_call_name
|
|
311
340
|
self.tools_call_ids = tools_call_ids
|
|
312
341
|
self.tools_call_extra_content = tools_call_extra_content
|
|
342
|
+
self.reasoning_content = reasoning_content
|
|
343
|
+
self.reasoning_signature = reasoning_signature
|
|
313
344
|
self.raw_completion = raw_completion
|
|
314
345
|
self.is_chunk = is_chunk
|
|
315
346
|
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
from collections.abc import AsyncGenerator
|
|
5
5
|
from typing import TypeAlias, Union
|
|
6
6
|
|
|
7
|
-
from astrbot.core.agent.message import Message
|
|
7
|
+
from astrbot.core.agent.message import ContentPart, Message
|
|
8
8
|
from astrbot.core.agent.tool import ToolSet
|
|
9
9
|
from astrbot.core.provider.entities import (
|
|
10
10
|
LLMResponse,
|
|
@@ -103,6 +103,7 @@ class Provider(AbstractProvider):
|
|
|
103
103
|
system_prompt: str | None = None,
|
|
104
104
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
|
105
105
|
model: str | None = None,
|
|
106
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
106
107
|
**kwargs,
|
|
107
108
|
) -> LLMResponse:
|
|
108
109
|
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
|
|
@@ -114,6 +115,7 @@ class Provider(AbstractProvider):
|
|
|
114
115
|
tools: tool set
|
|
115
116
|
contexts: 上下文,和 prompt 二选一使用
|
|
116
117
|
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
|
118
|
+
extra_user_content_parts: 额外的内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等)
|
|
117
119
|
kwargs: 其他参数
|
|
118
120
|
|
|
119
121
|
Notes:
|
|
@@ -11,6 +11,7 @@ from anthropic.types.usage import Usage
|
|
|
11
11
|
|
|
12
12
|
from astrbot import logger
|
|
13
13
|
from astrbot.api.provider import Provider
|
|
14
|
+
from astrbot.core.agent.message import ContentPart, ImageURLPart, TextPart
|
|
14
15
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
|
15
16
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
|
16
17
|
from astrbot.core.utils.io import download_image_by_url
|
|
@@ -47,6 +48,8 @@ class ProviderAnthropic(Provider):
|
|
|
47
48
|
base_url=self.base_url,
|
|
48
49
|
)
|
|
49
50
|
|
|
51
|
+
self.thinking_config = provider_config.get("anth_thinking_config", {})
|
|
52
|
+
|
|
50
53
|
self.set_model(provider_config.get("model", "unknown"))
|
|
51
54
|
|
|
52
55
|
def _prepare_payload(self, messages: list[dict]):
|
|
@@ -63,12 +66,33 @@ class ProviderAnthropic(Provider):
|
|
|
63
66
|
new_messages = []
|
|
64
67
|
for message in messages:
|
|
65
68
|
if message["role"] == "system":
|
|
66
|
-
system_prompt = message["content"]
|
|
69
|
+
system_prompt = message["content"] or "<empty system prompt>"
|
|
67
70
|
elif message["role"] == "assistant":
|
|
68
71
|
blocks = []
|
|
69
|
-
|
|
72
|
+
reasoning_content = ""
|
|
73
|
+
thinking_signature = ""
|
|
74
|
+
if isinstance(message["content"], str) and message["content"].strip():
|
|
70
75
|
blocks.append({"type": "text", "text": message["content"]})
|
|
71
|
-
|
|
76
|
+
elif isinstance(message["content"], list):
|
|
77
|
+
for part in message["content"]:
|
|
78
|
+
if part.get("type") == "think":
|
|
79
|
+
# only pick the last think part for now
|
|
80
|
+
reasoning_content = part.get("think")
|
|
81
|
+
thinking_signature = part.get("encrypted")
|
|
82
|
+
else:
|
|
83
|
+
blocks.append(part)
|
|
84
|
+
|
|
85
|
+
if reasoning_content and thinking_signature:
|
|
86
|
+
blocks.insert(
|
|
87
|
+
0,
|
|
88
|
+
{
|
|
89
|
+
"type": "thinking",
|
|
90
|
+
"thinking": reasoning_content,
|
|
91
|
+
"signature": thinking_signature,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if "tool_calls" in message and isinstance(message["tool_calls"], list):
|
|
72
96
|
for tool_call in message["tool_calls"]:
|
|
73
97
|
blocks.append( # noqa: PERF401
|
|
74
98
|
{
|
|
@@ -99,7 +123,7 @@ class ProviderAnthropic(Provider):
|
|
|
99
123
|
{
|
|
100
124
|
"type": "tool_result",
|
|
101
125
|
"tool_use_id": message["tool_call_id"],
|
|
102
|
-
"content": message["content"],
|
|
126
|
+
"content": message["content"] or "<empty response>",
|
|
103
127
|
},
|
|
104
128
|
],
|
|
105
129
|
},
|
|
@@ -132,6 +156,14 @@ class ProviderAnthropic(Provider):
|
|
|
132
156
|
|
|
133
157
|
extra_body = self.provider_config.get("custom_extra_body", {})
|
|
134
158
|
|
|
159
|
+
if "max_tokens" not in payloads:
|
|
160
|
+
payloads["max_tokens"] = 1024
|
|
161
|
+
if self.thinking_config.get("budget"):
|
|
162
|
+
payloads["thinking"] = {
|
|
163
|
+
"budget_tokens": self.thinking_config.get("budget"),
|
|
164
|
+
"type": "enabled",
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
completion = await self.client.messages.create(
|
|
136
168
|
**payloads, stream=False, extra_body=extra_body
|
|
137
169
|
)
|
|
@@ -149,6 +181,11 @@ class ProviderAnthropic(Provider):
|
|
|
149
181
|
completion_text = str(content_block.text).strip()
|
|
150
182
|
llm_response.completion_text = completion_text
|
|
151
183
|
|
|
184
|
+
if content_block.type == "thinking":
|
|
185
|
+
reasoning_content = str(content_block.thinking).strip()
|
|
186
|
+
llm_response.reasoning_content = reasoning_content
|
|
187
|
+
llm_response.reasoning_signature = content_block.signature
|
|
188
|
+
|
|
152
189
|
if content_block.type == "tool_use":
|
|
153
190
|
llm_response.tools_call_args.append(content_block.input)
|
|
154
191
|
llm_response.tools_call_name.append(content_block.name)
|
|
@@ -180,6 +217,16 @@ class ProviderAnthropic(Provider):
|
|
|
180
217
|
id = None
|
|
181
218
|
usage = TokenUsage()
|
|
182
219
|
extra_body = self.provider_config.get("custom_extra_body", {})
|
|
220
|
+
reasoning_content = ""
|
|
221
|
+
reasoning_signature = ""
|
|
222
|
+
|
|
223
|
+
if "max_tokens" not in payloads:
|
|
224
|
+
payloads["max_tokens"] = 1024
|
|
225
|
+
if self.thinking_config.get("budget"):
|
|
226
|
+
payloads["thinking"] = {
|
|
227
|
+
"budget_tokens": self.thinking_config.get("budget"),
|
|
228
|
+
"type": "enabled",
|
|
229
|
+
}
|
|
183
230
|
|
|
184
231
|
async with self.client.messages.stream(
|
|
185
232
|
**payloads, extra_body=extra_body
|
|
@@ -219,6 +266,21 @@ class ProviderAnthropic(Provider):
|
|
|
219
266
|
usage=usage,
|
|
220
267
|
id=id,
|
|
221
268
|
)
|
|
269
|
+
elif event.delta.type == "thinking_delta":
|
|
270
|
+
# 思考增量
|
|
271
|
+
reasoning = event.delta.thinking
|
|
272
|
+
if reasoning:
|
|
273
|
+
yield LLMResponse(
|
|
274
|
+
role="assistant",
|
|
275
|
+
reasoning_content=reasoning,
|
|
276
|
+
is_chunk=True,
|
|
277
|
+
usage=usage,
|
|
278
|
+
id=id,
|
|
279
|
+
reasoning_signature=reasoning_signature or None,
|
|
280
|
+
)
|
|
281
|
+
reasoning_content += reasoning
|
|
282
|
+
elif event.delta.type == "signature_delta":
|
|
283
|
+
reasoning_signature = event.delta.signature
|
|
222
284
|
elif event.delta.type == "input_json_delta":
|
|
223
285
|
# 工具调用参数增量
|
|
224
286
|
if event.index in tool_use_buffer:
|
|
@@ -275,6 +337,8 @@ class ProviderAnthropic(Provider):
|
|
|
275
337
|
is_chunk=False,
|
|
276
338
|
usage=usage,
|
|
277
339
|
id=id,
|
|
340
|
+
reasoning_content=reasoning_content,
|
|
341
|
+
reasoning_signature=reasoning_signature or None,
|
|
278
342
|
)
|
|
279
343
|
|
|
280
344
|
if final_tool_calls:
|
|
@@ -296,13 +360,16 @@ class ProviderAnthropic(Provider):
|
|
|
296
360
|
system_prompt=None,
|
|
297
361
|
tool_calls_result=None,
|
|
298
362
|
model=None,
|
|
363
|
+
extra_user_content_parts=None,
|
|
299
364
|
**kwargs,
|
|
300
365
|
) -> LLMResponse:
|
|
301
366
|
if contexts is None:
|
|
302
367
|
contexts = []
|
|
303
368
|
new_record = None
|
|
304
369
|
if prompt is not None:
|
|
305
|
-
new_record = await self.assemble_context(
|
|
370
|
+
new_record = await self.assemble_context(
|
|
371
|
+
prompt, image_urls, extra_user_content_parts
|
|
372
|
+
)
|
|
306
373
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
307
374
|
if new_record:
|
|
308
375
|
context_query.append(new_record)
|
|
@@ -342,21 +409,24 @@ class ProviderAnthropic(Provider):
|
|
|
342
409
|
|
|
343
410
|
async def text_chat_stream(
|
|
344
411
|
self,
|
|
345
|
-
prompt,
|
|
412
|
+
prompt=None,
|
|
346
413
|
session_id=None,
|
|
347
|
-
image_urls
|
|
414
|
+
image_urls=None,
|
|
348
415
|
func_tool=None,
|
|
349
|
-
contexts
|
|
416
|
+
contexts=None,
|
|
350
417
|
system_prompt=None,
|
|
351
418
|
tool_calls_result=None,
|
|
352
419
|
model=None,
|
|
420
|
+
extra_user_content_parts=None,
|
|
353
421
|
**kwargs,
|
|
354
422
|
):
|
|
355
423
|
if contexts is None:
|
|
356
424
|
contexts = []
|
|
357
425
|
new_record = None
|
|
358
426
|
if prompt is not None:
|
|
359
|
-
new_record = await self.assemble_context(
|
|
427
|
+
new_record = await self.assemble_context(
|
|
428
|
+
prompt, image_urls, extra_user_content_parts
|
|
429
|
+
)
|
|
360
430
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
361
431
|
if new_record:
|
|
362
432
|
context_query.append(new_record)
|
|
@@ -388,15 +458,15 @@ class ProviderAnthropic(Provider):
|
|
|
388
458
|
async for llm_response in self._query_stream(payloads, func_tool):
|
|
389
459
|
yield llm_response
|
|
390
460
|
|
|
391
|
-
async def assemble_context(
|
|
461
|
+
async def assemble_context(
|
|
462
|
+
self,
|
|
463
|
+
text: str,
|
|
464
|
+
image_urls: list[str] | None = None,
|
|
465
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
466
|
+
):
|
|
392
467
|
"""组装上下文,支持文本和图片"""
|
|
393
|
-
if not image_urls:
|
|
394
|
-
return {"role": "user", "content": text}
|
|
395
468
|
|
|
396
|
-
|
|
397
|
-
content.append({"type": "text", "text": text})
|
|
398
|
-
|
|
399
|
-
for image_url in image_urls:
|
|
469
|
+
async def resolve_image_url(image_url: str) -> dict | None:
|
|
400
470
|
if image_url.startswith("http"):
|
|
401
471
|
image_path = await download_image_by_url(image_url)
|
|
402
472
|
image_data = await self.encode_image_bs64(image_path)
|
|
@@ -408,28 +478,68 @@ class ProviderAnthropic(Provider):
|
|
|
408
478
|
|
|
409
479
|
if not image_data:
|
|
410
480
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
411
|
-
|
|
481
|
+
return None
|
|
412
482
|
|
|
413
483
|
# Get mime type for the image
|
|
414
484
|
mime_type, _ = guess_type(image_url)
|
|
415
485
|
if not mime_type:
|
|
416
486
|
mime_type = "image/jpeg" # Default to JPEG if can't determine
|
|
417
487
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
"
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
),
|
|
429
|
-
},
|
|
488
|
+
return {
|
|
489
|
+
"type": "image",
|
|
490
|
+
"source": {
|
|
491
|
+
"type": "base64",
|
|
492
|
+
"media_type": mime_type,
|
|
493
|
+
"data": (
|
|
494
|
+
image_data.split("base64,")[1]
|
|
495
|
+
if "base64," in image_data
|
|
496
|
+
else image_data
|
|
497
|
+
),
|
|
430
498
|
},
|
|
431
|
-
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
content = []
|
|
432
502
|
|
|
503
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
504
|
+
if text:
|
|
505
|
+
content.append({"type": "text", "text": text})
|
|
506
|
+
elif image_urls:
|
|
507
|
+
# 如果没有文本但有图片,添加占位文本
|
|
508
|
+
content.append({"type": "text", "text": "[图片]"})
|
|
509
|
+
elif extra_user_content_parts:
|
|
510
|
+
# 如果只有额外内容块,也需要添加占位文本
|
|
511
|
+
content.append({"type": "text", "text": " "})
|
|
512
|
+
|
|
513
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
514
|
+
if extra_user_content_parts:
|
|
515
|
+
for block in extra_user_content_parts:
|
|
516
|
+
if isinstance(block, TextPart):
|
|
517
|
+
content.append({"type": "text", "text": block.text})
|
|
518
|
+
elif isinstance(block, ImageURLPart):
|
|
519
|
+
image_dict = await resolve_image_url(block.image_url.url)
|
|
520
|
+
if image_dict:
|
|
521
|
+
content.append(image_dict)
|
|
522
|
+
else:
|
|
523
|
+
raise ValueError(f"不支持的额外内容块类型: {type(block)}")
|
|
524
|
+
|
|
525
|
+
# 3. 图片内容
|
|
526
|
+
if image_urls:
|
|
527
|
+
for image_url in image_urls:
|
|
528
|
+
image_dict = await resolve_image_url(image_url)
|
|
529
|
+
if image_dict:
|
|
530
|
+
content.append(image_dict)
|
|
531
|
+
|
|
532
|
+
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
|
533
|
+
if (
|
|
534
|
+
text
|
|
535
|
+
and not extra_user_content_parts
|
|
536
|
+
and not image_urls
|
|
537
|
+
and len(content) == 1
|
|
538
|
+
and content[0]["type"] == "text"
|
|
539
|
+
):
|
|
540
|
+
return {"role": "user", "content": content[0]["text"]}
|
|
541
|
+
|
|
542
|
+
# 否则返回多模态格式
|
|
433
543
|
return {"role": "user", "content": content}
|
|
434
544
|
|
|
435
545
|
async def encode_image_bs64(self, image_url: str) -> str:
|
|
@@ -56,10 +56,14 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
|
|
56
56
|
"api_base",
|
|
57
57
|
"https://api.fish-audio.cn/v1",
|
|
58
58
|
)
|
|
59
|
+
try:
|
|
60
|
+
self.timeout: int = int(provider_config.get("timeout", 20))
|
|
61
|
+
except ValueError:
|
|
62
|
+
self.timeout = 20
|
|
59
63
|
self.headers = {
|
|
60
64
|
"Authorization": f"Bearer {self.chosen_api_key}",
|
|
61
65
|
}
|
|
62
|
-
self.set_model(provider_config
|
|
66
|
+
self.set_model(provider_config.get("model", None))
|
|
63
67
|
|
|
64
68
|
async def _get_reference_id_by_character(self, character: str) -> str | None:
|
|
65
69
|
"""获取角色的reference_id
|
|
@@ -135,17 +139,21 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
|
|
135
139
|
path = os.path.join(temp_dir, f"fishaudio_tts_api_{uuid.uuid4()}.wav")
|
|
136
140
|
self.headers["content-type"] = "application/msgpack"
|
|
137
141
|
request = await self._generate_request(text)
|
|
138
|
-
async with AsyncClient(base_url=self.api_base).stream(
|
|
142
|
+
async with AsyncClient(base_url=self.api_base, timeout=self.timeout).stream(
|
|
139
143
|
"POST",
|
|
140
144
|
"/tts",
|
|
141
145
|
headers=self.headers,
|
|
142
146
|
content=ormsgpack.packb(request, option=ormsgpack.OPT_SERIALIZE_PYDANTIC),
|
|
143
147
|
) as response:
|
|
144
|
-
if response.
|
|
148
|
+
if response.status_code == 200 and response.headers.get(
|
|
149
|
+
"content-type", ""
|
|
150
|
+
).startswith("audio/"):
|
|
145
151
|
with open(path, "wb") as f:
|
|
146
152
|
async for chunk in response.aiter_bytes():
|
|
147
153
|
f.write(chunk)
|
|
148
154
|
return path
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
raise Exception(
|
|
155
|
+
error_bytes = await response.aread()
|
|
156
|
+
error_text = error_bytes.decode("utf-8", errors="replace")[:1024]
|
|
157
|
+
raise Exception(
|
|
158
|
+
f"Fish Audio API请求失败: 状态码 {response.status_code}, 响应内容: {error_text}"
|
|
159
|
+
)
|