Compare commits

...

6 Commits

Author SHA1 Message Date
Soulter 79e2743aac chore: bump version to 4.3.3 2025-10-12 11:42:18 +08:00
anka 5e9c7cdd91 fix: 当没有填写 api key 时,设置为空字符串 (#2834)
* fix: 修复空key导致的无法创建Provider对象的问题

* style: format code

* Update astrbot/core/provider/provider.py

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>
2025-10-12 10:50:01 +08:00
Dt8333 6f73e5087d feat(core): 在新对话中重用先前的对话人格设置 (#3005)
* feat(core): reuse persona conf in new conversation

#2985

* refactor(core): simplify persona retrieval logic

* style: code format

---------

Co-authored-by: Soulter <905617992@qq.com>
2025-10-12 10:42:35 +08:00
Yaron 8c120b020e fix: 修复阿里云百炼平台 TTS 下接入 CosyVoice V2, Qwen TTS 生成报错的问题 (#2964)
* fix: 修复了CosyVoice V2,Qwen TTS生成报错的问题。Fixed compatability problems with CosyVoice V2, Qwen TTS.

* fix: 将urlopen的同步请求替换为aiohttp的异步请求以下载音频

* fix: cozyvoice 报错显示

* fix: 添加阿里云百炼 TTS API Key 获取提示信息

---------

Co-authored-by: Soulter <905617992@qq.com>
2025-10-12 01:03:06 +08:00
Dt8333 12fc6f9d38 fix(LTM): fix LTM not removed when removing conversation (#3002)
#2983
2025-10-12 00:16:42 +08:00
Dt8333 a6e8483b4c fix: 修复session-management中人格错误的显示为默认人格的问题 (#3000)
* fix: 修复session-management中人格错误的显示为默认人格的问题

#2985

* refactor: 使用命名表达式简化赋值和条件

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* style: format edited code with ruff

format code edited by sourcery-ai

---------

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-10-12 00:12:04 +08:00
10 changed files with 202 additions and 59 deletions
+3 -6
View File
@@ -6,7 +6,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.3.2" VERSION = "4.3.3"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
# 默认配置 # 默认配置
@@ -1056,6 +1056,7 @@ CONFIG_METADATA_2 = {
"timeout": "20", "timeout": "20",
}, },
"阿里云百炼 TTS(API)": { "阿里云百炼 TTS(API)": {
"hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition",
"id": "dashscope_tts", "id": "dashscope_tts",
"provider": "dashscope", "provider": "dashscope",
"type": "dashscope_tts", "type": "dashscope_tts",
@@ -1435,11 +1436,7 @@ CONFIG_METADATA_2 = {
"description": "服务订阅密钥", "description": "服务订阅密钥",
"hint": "Azure_TTS 服务的订阅密钥(注意不是令牌)", "hint": "Azure_TTS 服务的订阅密钥(注意不是令牌)",
}, },
"dashscope_tts_voice": { "dashscope_tts_voice": {"description": "音色", "type": "string"},
"description": "语音合成模型",
"type": "string",
"hint": "阿里云百炼语音合成模型名称。具体可参考 https://help.aliyun.com/zh/model-studio/developer-reference/cosyvoice-python-api 等内容",
},
"gm_resp_image_modal": { "gm_resp_image_modal": {
"description": "启用图片模态", "description": "启用图片模态",
"type": "bool", "type": "bool",
+2 -1
View File
@@ -68,7 +68,8 @@ class Provider(AbstractProvider):
def get_keys(self) -> List[str]: def get_keys(self) -> List[str]:
"""获得提供商 Key""" """获得提供商 Key"""
return self.provider_config.get("key", []) keys = self.provider_config.get("key", [""])
return keys or [""]
@abc.abstractmethod @abc.abstractmethod
def set_key(self, key: str): def set_key(self, key: str):
@@ -33,7 +33,7 @@ class ProviderAnthropic(Provider):
) )
self.chosen_api_key: str = "" self.chosen_api_key: str = ""
self.api_keys: List = provider_config.get("key", []) self.api_keys: List = super().get_keys()
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else "" self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else ""
self.base_url = provider_config.get("api_base", "https://api.anthropic.com") self.base_url = provider_config.get("api_base", "https://api.anthropic.com")
self.timeout = provider_config.get("timeout", 120) self.timeout = provider_config.get("timeout", 120)
@@ -70,9 +70,13 @@ class ProviderAnthropic(Provider):
{ {
"type": "tool_use", "type": "tool_use",
"name": tool_call["function"]["name"], "name": tool_call["function"]["name"],
"input": json.loads(tool_call["function"]["arguments"]) "input": (
if isinstance(tool_call["function"]["arguments"], str) json.loads(tool_call["function"]["arguments"])
else tool_call["function"]["arguments"], if isinstance(
tool_call["function"]["arguments"], str
)
else tool_call["function"]["arguments"]
),
"id": tool_call["id"], "id": tool_call["id"],
} }
) )
@@ -355,9 +359,11 @@ class ProviderAnthropic(Provider):
"source": { "source": {
"type": "base64", "type": "base64",
"media_type": mime_type, "media_type": mime_type,
"data": image_data.split("base64,")[1] "data": (
if "base64," in image_data image_data.split("base64,")[1]
else image_data, if "base64," in image_data
else image_data
),
}, },
} }
) )
+121 -13
View File
@@ -1,10 +1,22 @@
import os
import dashscope
import uuid
import asyncio import asyncio
from dashscope.audio.tts_v2 import * import base64
from ..provider import TTSProvider import logging
import os
import uuid
from typing import Optional, Tuple
import aiohttp
import dashscope
from dashscope.audio.tts_v2 import AudioFormat, SpeechSynthesizer
try:
from dashscope.aigc.multimodal_conversation import MultiModalConversation
except (
ImportError
): # pragma: no cover - older dashscope versions without Qwen TTS support
MultiModalConversation = None
from ..entities import ProviderType from ..entities import ProviderType
from ..provider import TTSProvider
from ..register import register_provider_adapter from ..register import register_provider_adapter
from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.astrbot_path import get_astrbot_data_path
@@ -26,16 +38,112 @@ class ProviderDashscopeTTSAPI(TTSProvider):
dashscope.api_key = self.chosen_api_key dashscope.api_key = self.chosen_api_key
async def get_audio(self, text: str) -> str: async def get_audio(self, text: str) -> str:
model = self.get_model()
if not model:
raise RuntimeError("Dashscope TTS model is not configured.")
temp_dir = os.path.join(get_astrbot_data_path(), "temp") temp_dir = os.path.join(get_astrbot_data_path(), "temp")
path = os.path.join(temp_dir, f"dashscope_tts_{uuid.uuid4()}.wav") os.makedirs(temp_dir, exist_ok=True)
self.synthesizer = SpeechSynthesizer(
model=self.get_model(), if self._is_qwen_tts_model(model):
audio_bytes, ext = await self._synthesize_with_qwen_tts(model, text)
else:
audio_bytes, ext = await self._synthesize_with_cosyvoice(model, text)
if not audio_bytes:
raise RuntimeError(
"Audio synthesis failed, returned empty content. The model may not be supported or the service is unavailable."
)
path = os.path.join(temp_dir, f"dashscope_tts_{uuid.uuid4()}{ext}")
with open(path, "wb") as f:
f.write(audio_bytes)
return path
def _call_qwen_tts(self, model: str, text: str):
if MultiModalConversation is None:
raise RuntimeError(
"dashscope SDK missing MultiModalConversation. Please upgrade the dashscope package to use Qwen TTS models."
)
kwargs = {
"model": model,
"text": text,
"api_key": self.chosen_api_key,
"voice": self.voice or "Cherry",
}
if not self.voice:
logging.warning(
"No voice specified for Qwen TTS model, using default 'Cherry'."
)
return MultiModalConversation.call(**kwargs)
async def _synthesize_with_qwen_tts(
self, model: str, text: str
) -> Tuple[Optional[bytes], str]:
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, self._call_qwen_tts, model, text)
audio_bytes = await self._extract_audio_from_response(response)
if not audio_bytes:
raise RuntimeError(
f"Audio synthesis failed for model '{model}'. {response}"
)
ext = ".wav"
return audio_bytes, ext
async def _extract_audio_from_response(self, response) -> Optional[bytes]:
output = getattr(response, "output", None)
audio_obj = getattr(output, "audio", None) if output is not None else None
if not audio_obj:
return None
data_b64 = getattr(audio_obj, "data", None)
if data_b64:
try:
return base64.b64decode(data_b64)
except (ValueError, TypeError):
logging.error("Failed to decode base64 audio data.")
return None
url = getattr(audio_obj, "url", None)
if url:
return await self._download_audio_from_url(url)
return None
async def _download_audio_from_url(self, url: str) -> Optional[bytes]:
if not url:
return None
timeout = max(self.timeout_ms / 1000, 1) if self.timeout_ms else 20
try:
async with aiohttp.ClientSession() as session:
async with session.get(
url, timeout=aiohttp.ClientTimeout(total=timeout)
) as response:
return await response.read()
except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e:
logging.error(f"Failed to download audio from URL {url}: {e}")
return None
async def _synthesize_with_cosyvoice(
self, model: str, text: str
) -> Tuple[Optional[bytes], str]:
synthesizer = SpeechSynthesizer(
model=model,
voice=self.voice, voice=self.voice,
format=AudioFormat.WAV_24000HZ_MONO_16BIT, format=AudioFormat.WAV_24000HZ_MONO_16BIT,
) )
audio = await asyncio.get_event_loop().run_in_executor( loop = asyncio.get_event_loop()
None, self.synthesizer.call, text, self.timeout_ms audio_bytes = await loop.run_in_executor(
None, synthesizer.call, text, self.timeout_ms
) )
with open(path, "wb") as f: if not audio_bytes:
f.write(audio) resp = synthesizer.get_response()
return path if resp and isinstance(resp, dict):
raise RuntimeError(
f"Audio synthesis failed for model '{model}'. {resp}".strip()
)
return audio_bytes, ".wav"
def _is_qwen_tts_model(self, model: str) -> bool:
model_lower = model.lower()
return "tts" in model_lower and model_lower.startswith("qwen")
+21 -17
View File
@@ -3,7 +3,7 @@ import base64
import json import json
import logging import logging
import random import random
from typing import Optional from typing import Optional, List
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from google import genai from google import genai
@@ -60,7 +60,7 @@ class ProviderGoogleGenAI(Provider):
provider_settings, provider_settings,
default_persona, default_persona,
) )
self.api_keys: list = provider_config.get("key", []) self.api_keys: List = super().get_keys()
self.chosen_api_key: str = self.api_keys[0] if len(self.api_keys) > 0 else "" self.chosen_api_key: str = self.api_keys[0] if len(self.api_keys) > 0 else ""
self.timeout: int = int(provider_config.get("timeout", 180)) self.timeout: int = int(provider_config.get("timeout", 180))
@@ -218,19 +218,21 @@ class ProviderGoogleGenAI(Provider):
response_modalities=modalities, response_modalities=modalities,
tools=tool_list, tools=tool_list,
safety_settings=self.safety_settings if self.safety_settings else None, safety_settings=self.safety_settings if self.safety_settings else None,
thinking_config=types.ThinkingConfig( thinking_config=(
thinking_budget=min( types.ThinkingConfig(
int( thinking_budget=min(
self.provider_config.get("gm_thinking_config", {}).get( int(
"budget", 0 self.provider_config.get("gm_thinking_config", {}).get(
) "budget", 0
)
),
24576,
), ),
24576, )
), if "gemini-2.5-flash" in self.get_model()
) and hasattr(types.ThinkingConfig, "thinking_budget")
if "gemini-2.5-flash" in self.get_model() else None
and hasattr(types.ThinkingConfig, "thinking_budget") ),
else None,
automatic_function_calling=types.AutomaticFunctionCallingConfig( automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True disable=True
), ),
@@ -274,9 +276,11 @@ class ProviderGoogleGenAI(Provider):
if role == "user": if role == "user":
if isinstance(content, list): if isinstance(content, list):
parts = [ parts = [
types.Part.from_text(text=item["text"] or " ") (
if item["type"] == "text" types.Part.from_text(text=item["text"] or " ")
else process_image_url(item["image_url"]) if item["type"] == "text"
else process_image_url(item["image_url"])
)
for item in content for item in content
] ]
else: else:
@@ -38,7 +38,7 @@ class ProviderOpenAIOfficial(Provider):
default_persona, default_persona,
) )
self.chosen_api_key = None self.chosen_api_key = None
self.api_keys: List = provider_config.get("key", []) self.api_keys: List = super().get_keys()
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else None self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else None
self.timeout = provider_config.get("timeout", 120) self.timeout = provider_config.get("timeout", 120)
if isinstance(self.timeout, str): if isinstance(self.timeout, str):
@@ -65,12 +65,12 @@ class SessionManagementRoute(Route):
persona_name = data["persona_name"] persona_name = data["persona_name"]
# 处理 persona 显示 # 处理 persona 显示
if conv_persona_id == "[%None]": if persona_name is None:
persona_name = "无人格" if conv_persona_id is None:
else: if default_persona := persona_mgr.selected_default_persona_v3:
default_persona = persona_mgr.selected_default_persona_v3 persona_name = default_persona["name"]
if default_persona: else:
persona_name = default_persona["name"] persona_name = "[%None]"
session_info = { session_info = {
"session_id": session_id, "session_id": session_id,
+12
View File
@@ -0,0 +1,12 @@
# What's Changed
1. fix: 修复了代码执行器插件不能正确获得发送来文件的问题 ([#2970](https://github.com/Soulter/AstrBot/issues/2970))
2. fix: 修改的 DeepSeek 默认 modalities,避免默认勾选图像导致的报错。 ([#2963](https://github.com/Soulter/AstrBot/issues/2963))
3. fix: 事件钩子终止事件传播后不继续执行 ([#2989](https://github.com/Soulter/AstrBot/issues/2989))
4. fix: 启动了 TTS 但未配置 TTS 模型时,At 和 Reply 发送人无效
5. fix: 修复 session-management 中人格错误的显示为默认人格的问题 ([#3000](https://github.com/Soulter/AstrBot/issues/3000))
6. fix: 修复了删除对话时,聊天增强中的记录未被清除,导致新对话中仍然出现之前的聊天记录。 ([#3002](https://github.com/Soulter/AstrBot/issues/3002))
7. fix: 修复阿里云百炼平台 TTS 下接入 CosyVoice V2, Qwen TTS 生成报错的问题 ([#2964](https://github.com/Soulter/AstrBot/issues/2964))
8. perf: 优化 SQLite 参数配置,对话和会话管理增加输入防抖机制 ([#2969](https://github.com/Soulter/AstrBot/issues/2969))
9. feat: 在新对话中重用先前的对话人格设置 ([#3005](https://github.com/Soulter/AstrBot/issues/3005))
10. feat: 从 WebUI 更新后清除浏览器缓存 ([#2958](https://github.com/Soulter/AstrBot/issues/2958))
+22 -7
View File
@@ -41,6 +41,17 @@ class ConversationCommands:
self.context = context self.context = context
self.ltm = ltm self.ltm = ltm
async def _get_current_persona_id(self, session_id):
curr = await self.context.conversation_manager.get_curr_conversation_id(
session_id
)
if not curr:
return None
conv = await self.context.conversation_manager.get_conversation(
session_id, curr
)
return conv.persona_id
def ltm_enabled(self, event: AstrMessageEvent): def ltm_enabled(self, event: AstrMessageEvent):
if not self.ltm: if not self.ltm:
return False return False
@@ -255,8 +266,9 @@ class ConversationCommands:
) )
return return
cpersona = await self._get_current_persona_id(message.unified_msg_origin)
cid = await self.context.conversation_manager.new_conversation( cid = await self.context.conversation_manager.new_conversation(
message.unified_msg_origin, message.get_platform_id() message.unified_msg_origin, message.get_platform_id(), persona_id=cpersona
) )
# 长期记忆 # 长期记忆
@@ -290,8 +302,10 @@ class ConversationCommands:
session_id=sid, session_id=sid,
) )
) )
cpersona = await self._get_current_persona_id(session)
cid = await self.context.conversation_manager.new_conversation( cid = await self.context.conversation_manager.new_conversation(
session, message.get_platform_id() session, message.get_platform_id(), persona_id=cpersona
) )
message.set_result( message.set_result(
MessageEventResult().message( MessageEventResult().message(
@@ -434,8 +448,9 @@ class ConversationCommands:
await self.context.conversation_manager.delete_conversation( await self.context.conversation_manager.delete_conversation(
message.unified_msg_origin, session_curr_cid message.unified_msg_origin, session_curr_cid
) )
message.set_result(
MessageEventResult().message( ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"
"删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。" if self.ltm and self.ltm_enabled(message):
) cnt = await self.ltm.remove_session(event=message)
) ret += f"\n聊天增强: 已清除 {cnt} 条聊天记录。"
message.set_result(MessageEventResult().message(ret))
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "AstrBot" name = "AstrBot"
version = "4.3.2" version = "4.3.3"
description = "易上手的多平台 LLM 聊天机器人及开发框架" description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"