Compare commits

...

22 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
Soulter 7191d28ada fix: 启动了 TTS 但未配置 TTS 模型时,At 和 Reply 发送人无效
fixes: #2996
2025-10-10 12:11:03 +08:00
Soulter e6b5e3d282 feat: tokenpony provider 2025-10-09 16:00:31 +08:00
ctrlkk 1413d6b5fe fix: 让事件钩子被暂停时跳出循环,而不是继续执行 (#2989) 2025-10-09 15:01:45 +08:00
ctrlkk dcd8a1094c feat: 优化 SQLite 参数配置,对话和会话管理增加输入防抖机制 (#2969)
* feat: 优化 SQLite 数据库初始化设置并增强会话搜索功能,会话管理增加输入防抖

* fix: adjust SQLite cache and mmap size

---------

Co-authored-by: Soulter <905617992@qq.com>
2025-10-06 17:13:53 +08:00
Futureppo e64b31b9ba fix: Correct default modalities for DeepSeek provider (#2963)
* 更新 package.json

* 更新 ExtensionPage.vue

* fix(provider): Correct default modalities for DeepSeek provider
2025-10-06 16:30:05 +08:00
Dt8333 080f347511 feat: clean browser cache after update (#2958)
* feat: clean browser cache after update

* fix: move const to module

* fix: remove self prefix (a stupid mistake)
2025-10-06 16:29:18 +08:00
Dt8333 eaaff4298d fix(Python-Interpreter): fix incorrect file read method (#2970)
fix getting file by property(Sync) in an async handler

#2960
2025-10-06 16:12:05 +08:00
Soulter dd5a02e8ef chore: bump version to 4.3.2 2025-10-05 01:01:13 +08:00
Soulter 3211ec57ee fix: handle Google search initialization and errors gracefully 2025-10-05 00:55:47 +08:00
Soulter 6796afdaee fix: googlesearch 2025-10-05 00:54:24 +08:00
Soulter cc6fe57773 fix: on_tool_end无法获得工具返回的结果 (#2956)
fixes: #2940
2025-10-05 00:37:51 +08:00
Soulter 1dfc831938 fix: 修复 reset 没有清除群聊上下文感知数据的问题 (#2954) 2025-10-05 00:05:42 +08:00
Futureppo cafeda4abf feat: 为插件市场的搜索增加拼音与首字母搜索功能 (#2936)
* 更新 package.json

* 更新 ExtensionPage.vue
2025-10-03 09:42:57 +08:00
Soulter d951b99718 fix: 发送阶段将 Plain 为空的消息段移除 2025-10-03 00:45:07 +08:00
Soulter 0ad87209e5 chore: bump version to 4.3.1 2025-10-02 17:25:09 +08:00
Soulter 1b50c5404d fix: enhance knowledge base plugin status check to handle empty data response 2025-10-02 17:25:00 +08:00
27 changed files with 465 additions and 219 deletions
@@ -198,6 +198,17 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
func_tool = req.func_tool.get_func(func_tool_name) func_tool = req.func_tool.get_func(func_tool_name)
logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}") logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
if not func_tool:
logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。")
tool_call_result_blocks.append(
ToolCallMessageSegment(
role="tool",
tool_call_id=func_tool_id,
content=f"error: 未找到工具 {func_tool_name}",
)
)
continue
try: try:
await self.agent_hooks.on_tool_start( await self.agent_hooks.on_tool_start(
self.run_context, func_tool, func_tool_args self.run_context, func_tool, func_tool_args
@@ -210,9 +221,12 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
run_context=self.run_context, run_context=self.run_context,
**func_tool_args, **func_tool_args,
) )
async for resp in executor:
_final_resp: CallToolResult | None = None
async for resp in executor: # type: ignore
if isinstance(resp, CallToolResult): if isinstance(resp, CallToolResult):
res = resp res = resp
_final_resp = resp
if isinstance(res.content[0], TextContent): if isinstance(res.content[0], TextContent):
tool_call_result_blocks.append( tool_call_result_blocks.append(
ToolCallMessageSegment( ToolCallMessageSegment(
@@ -279,13 +293,14 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
chain=res.chain, type="tool_direct_result" chain=res.chain, type="tool_direct_result"
) )
else: else:
# 不应该出现其他类型
logger.warning( logger.warning(
f"Tool 返回了不支持的类型: {type(resp)},将忽略。" f"Tool 返回了不支持的类型: {type(resp)},将忽略。"
) )
try: try:
await self.agent_hooks.on_tool_end( await self.agent_hooks.on_tool_end(
self.run_context, func_tool, func_tool_args, None self.run_context, func_tool, func_tool_args, _final_resp
) )
except Exception as e: except Exception as e:
logger.error(f"Error in on_tool_end hook: {e}", exc_info=True) logger.error(f"Error in on_tool_end hook: {e}", exc_info=True)
+19 -7
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.0" 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")
# 默认配置 # 默认配置
@@ -775,7 +775,7 @@ CONFIG_METADATA_2 = {
"timeout": 120, "timeout": 120,
"model_config": {"model": "deepseek-chat", "temperature": 0.4}, "model_config": {"model": "deepseek-chat", "temperature": 0.4},
"custom_extra_body": {}, "custom_extra_body": {},
"modalities": ["text", "image", "tool_use"], "modalities": ["text", "tool_use"],
}, },
"302.AI": { "302.AI": {
"id": "302ai", "id": "302ai",
@@ -821,6 +821,21 @@ CONFIG_METADATA_2 = {
}, },
"custom_extra_body": {}, "custom_extra_body": {},
}, },
"小马算力": {
"id": "tokenpony",
"provider": "tokenpony",
"type": "openai_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"api_base": "https://api.tokenpony.cn/v1",
"timeout": 120,
"model_config": {
"model": "kimi-k2-instruct-0905",
"temperature": 0.7,
},
"custom_extra_body": {},
},
"优云智算": { "优云智算": {
"id": "compshare", "id": "compshare",
"provider": "compshare", "provider": "compshare",
@@ -1041,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",
@@ -1420,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",
+7
View File
@@ -32,6 +32,12 @@ class SQLiteDatabase(BaseDatabase):
"""Initialize the database by creating tables if they do not exist.""" """Initialize the database by creating tables if they do not exist."""
async with self.engine.begin() as conn: async with self.engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all) await conn.run_sync(SQLModel.metadata.create_all)
await conn.execute(text("PRAGMA journal_mode=WAL"))
await conn.execute(text("PRAGMA synchronous=NORMAL"))
await conn.execute(text("PRAGMA cache_size=20000"))
await conn.execute(text("PRAGMA temp_store=MEMORY"))
await conn.execute(text("PRAGMA mmap_size=134217728"))
await conn.execute(text("PRAGMA optimize"))
await conn.commit() await conn.commit()
# ==== # ====
@@ -160,6 +166,7 @@ class SQLiteDatabase(BaseDatabase):
col(ConversationV2.title).ilike(f"%{search_query}%"), col(ConversationV2.title).ilike(f"%{search_query}%"),
col(ConversationV2.content).ilike(f"%{search_query}%"), col(ConversationV2.content).ilike(f"%{search_query}%"),
col(ConversationV2.user_id).ilike(f"%{search_query}%"), col(ConversationV2.user_id).ilike(f"%{search_query}%"),
col(ConversationV2.conversation_id).ilike(f"%{search_query}%"),
) )
) )
if "message_types" in kwargs and len(kwargs["message_types"]) > 0: if "message_types" in kwargs and len(kwargs["message_types"]) > 0:
+1
View File
@@ -97,5 +97,6 @@ async def call_event_hook(
logger.info( logger.info(
f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。" f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
) )
return True
return event.is_stopped() return event.is_stopped()
+10
View File
@@ -190,6 +190,16 @@ class RespondStage(Stage):
except Exception as e: except Exception as e:
logger.warning(f"空内容检查异常: {e}") logger.warning(f"空内容检查异常: {e}")
# 将 Plain 为空的消息段移除
result.chain = [
comp
for comp in result.chain
if not (
isinstance(comp, Comp.Plain)
and (not comp.text or not comp.text.strip())
)
]
# 发送消息链 # 发送消息链
# Record 需要强制单独发送 # Record 需要强制单独发送
need_separately = {ComponentType.Record} need_separately = {ComponentType.Record}
+47 -48
View File
@@ -189,54 +189,54 @@ class ResultDecorateStage(Stage):
logger.warning( logger.warning(
f"会话 {event.unified_msg_origin} 未配置文本转语音模型。" f"会话 {event.unified_msg_origin} 未配置文本转语音模型。"
) )
return else:
new_chain = [] new_chain = []
for comp in result.chain: for comp in result.chain:
if isinstance(comp, Plain) and len(comp.text) > 1: if isinstance(comp, Plain) and len(comp.text) > 1:
try: try:
logger.info(f"TTS 请求: {comp.text}") logger.info(f"TTS 请求: {comp.text}")
audio_path = await tts_provider.get_audio(comp.text) audio_path = await tts_provider.get_audio(comp.text)
logger.info(f"TTS 结果: {audio_path}") logger.info(f"TTS 结果: {audio_path}")
if not audio_path: if not audio_path:
logger.error( logger.error(
f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}" f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}"
)
new_chain.append(comp)
continue
use_file_service = self.ctx.astrbot_config[
"provider_tts_settings"
]["use_file_service"]
callback_api_base = self.ctx.astrbot_config[
"callback_api_base"
]
dual_output = self.ctx.astrbot_config[
"provider_tts_settings"
]["dual_output"]
url = None
if use_file_service and callback_api_base:
token = await file_token_service.register_file(
audio_path
)
url = f"{callback_api_base}/api/file/{token}"
logger.debug(f"已注册:{url}")
new_chain.append(
Record(
file=url or audio_path,
url=url or audio_path,
)
) )
if dual_output:
new_chain.append(comp)
except Exception:
logger.error(traceback.format_exc())
logger.error("TTS 失败,使用文本发送。")
new_chain.append(comp) new_chain.append(comp)
continue else:
use_file_service = self.ctx.astrbot_config[
"provider_tts_settings"
]["use_file_service"]
callback_api_base = self.ctx.astrbot_config[
"callback_api_base"
]
dual_output = self.ctx.astrbot_config[
"provider_tts_settings"
]["dual_output"]
url = None
if use_file_service and callback_api_base:
token = await file_token_service.register_file(
audio_path
)
url = f"{callback_api_base}/api/file/{token}"
logger.debug(f"已注册:{url}")
new_chain.append(
Record(
file=url or audio_path,
url=url or audio_path,
)
)
if dual_output:
new_chain.append(comp)
except Exception:
logger.error(traceback.format_exc())
logger.error("TTS 失败,使用文本发送。")
new_chain.append(comp) new_chain.append(comp)
else: result.chain = new_chain
new_chain.append(comp)
result.chain = new_chain
# 文本转图片 # 文本转图片
elif ( elif (
@@ -279,7 +279,6 @@ class ResultDecorateStage(Stage):
result.chain = [Image.fromFileSystem(url)] result.chain = [Image.fromFileSystem(url)]
# 触发转发消息 # 触发转发消息
has_forwarded = False
if event.get_platform_name() == "aiocqhttp": if event.get_platform_name() == "aiocqhttp":
word_cnt = 0 word_cnt = 0
for comp in result.chain: for comp in result.chain:
@@ -290,9 +289,9 @@ class ResultDecorateStage(Stage):
uin=event.get_self_id(), name="AstrBot", content=[*result.chain] uin=event.get_self_id(), name="AstrBot", content=[*result.chain]
) )
result.chain = [node] result.chain = [node]
has_forwarded = True
if not has_forwarded: has_plain = any(isinstance(item, Plain) for item in result.chain)
if has_plain:
# at 回复 # at 回复
if ( if (
self.reply_with_mention self.reply_with_mention
+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,
+8 -5
View File
@@ -9,6 +9,8 @@ from astrbot.core.config.default import VERSION
from astrbot.core import DEMO_MODE from astrbot.core import DEMO_MODE
from astrbot.core.db.migration.helper import do_migration_v4, check_migration_needed_v4 from astrbot.core.db.migration.helper import do_migration_v4, check_migration_needed_v4
CLEAR_SITE_DATA_HEADERS = {"Clear-Site-Data": '"cache"'}
class UpdateRoute(Route): class UpdateRoute(Route):
def __init__( def __init__(
@@ -113,17 +115,19 @@ class UpdateRoute(Route):
if reboot: if reboot:
await self.core_lifecycle.restart() await self.core_lifecycle.restart()
return ( ret = (
Response() Response()
.ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。") .ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。")
.__dict__ .__dict__
) )
return ret, 200, CLEAR_SITE_DATA_HEADERS
else: else:
return ( ret = (
Response() Response()
.ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。") .ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。")
.__dict__ .__dict__
) )
return ret, 200, CLEAR_SITE_DATA_HEADERS
except Exception as e: except Exception as e:
logger.error(f"/api/update_project: {traceback.format_exc()}") logger.error(f"/api/update_project: {traceback.format_exc()}")
return Response().error(e.__str__()).__dict__ return Response().error(e.__str__()).__dict__
@@ -135,9 +139,8 @@ class UpdateRoute(Route):
except Exception as e: except Exception as e:
logger.error(f"下载管理面板文件失败: {e}") logger.error(f"下载管理面板文件失败: {e}")
return Response().error(f"下载管理面板文件失败: {e}").__dict__ return Response().error(f"下载管理面板文件失败: {e}").__dict__
return ( ret = Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__
Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__ return ret, 200, CLEAR_SITE_DATA_HEADERS
)
except Exception as e: except Exception as e:
logger.error(f"/api/update_dashboard: {traceback.format_exc()}") logger.error(f"/api/update_dashboard: {traceback.format_exc()}")
return Response().error(e.__str__()).__dict__ return Response().error(e.__str__()).__dict__
+1
View File
@@ -0,0 +1 @@
# What's Changed
+7
View File
@@ -0,0 +1,7 @@
# What's Changed
1. fix: 修复 /reset 指令没有清除群聊上下文感知数据的问题 ([#2954](https://github.com/AstrBotDevs/AstrBot/issues/2954))
2. fix: 修复自带的 WebSearch 插件可能在部分场景下无法使用的问题
3. fix: 发送阶段强行将 Plain 为空的消息段移除
4. fix: on_tool_end无法获得工具返回的结果 ([#2956](https://github.com/AstrBotDevs/AstrBot/issues/2956))
5. feat: 为插件市场的搜索增加拼音与首字母搜索功能 ([#2936](https://github.com/AstrBotDevs/AstrBot/issues/2936))
+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))
+1
View File
@@ -27,6 +27,7 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"marked": "^15.0.7", "marked": "^15.0.7",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pinyin-pro": "^3.26.0",
"pinia": "2.1.6", "pinia": "2.1.6",
"remixicon": "3.5.0", "remixicon": "3.5.0",
"vee-validate": "4.11.3", "vee-validate": "4.11.3",
+95 -86
View File
@@ -10,7 +10,7 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-combobox v-model="platformFilter" :label="tm('filters.platform')" <v-combobox v-model="platformFilter" :label="tm('filters.platform')"
:items="availablePlatforms" chips multiple clearable variant="solo-filled" flat :items="availablePlatforms" chips multiple clearable variant="solo-filled" flat
density="compact" hide-details :disabled="loading"> density="compact" hide-details>
<template v-slot:selection="{ item }"> <template v-slot:selection="{ item }">
<v-chip size="small" label> <v-chip size="small" label>
{{ item.title }} {{ item.title }}
@@ -21,8 +21,7 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems" <v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems"
chips multiple clearable variant="solo-filled" density="compact" hide-details flat chips multiple clearable variant="solo-filled" density="compact" hide-details flat>
:disabled="loading">
<template v-slot:selection="{ item }"> <template v-slot:selection="{ item }">
<v-chip size="small" variant="solo-filled" label> <v-chip size="small" variant="solo-filled" label>
{{ item.title }} {{ item.title }}
@@ -34,7 +33,7 @@
<v-col cols="12" sm="12" md="4"> <v-col cols="12" sm="12" md="4">
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify" <v-text-field v-model="search" prepend-inner-icon="mdi-magnify"
:label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat :label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat
clearable :disabled="loading"></v-text-field> clearable></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations" <v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
@@ -79,6 +78,10 @@
</v-chip> </v-chip>
</template> </template>
<template v-slot:item.cid="{ item }">
<span class="text-truncate">{{ item.cid || tm('status.unknown') }}</span>
</template>
<template v-slot:item.sessionId="{ item }"> <template v-slot:item.sessionId="{ item }">
<span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span> <span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
</template> </template>
@@ -313,6 +316,7 @@
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { debounce } from 'lodash';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'; import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import { useCommonStore } from '@/stores/common'; import { useCommonStore } from '@/stores/common';
@@ -417,8 +421,7 @@ export default {
}, },
created() { created() {
// this.debouncedApplyFilters = debounce(() => {
this.debouncedApplyFilters = this.debounce(() => {
// //
this.pagination.page = 1; this.pagination.page = 1;
this.fetchConversations(); this.fetchConversations();
@@ -430,13 +433,14 @@ export default {
tableHeaders() { tableHeaders() {
return [ return [
{ title: this.tm('table.headers.title'), key: 'title', sortable: true }, { title: this.tm('table.headers.title'), key: 'title', sortable: true },
{ title: '会话 ID', key: 'cid', sortable: true, width: '100px' },
{ {
title: this.tm('table.headers.sessionId'), title: this.tm('table.headers.sessionId'),
align: 'center', align: 'center',
children: [ children: [
{ title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' }, { title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' },
{ title: this.tm('table.headers.type'), key: 'messageType', sortable: true, width: '100px' }, { title: this.tm('table.headers.type'), key: 'messageType', sortable: true, width: '100px' },
{ title: '会话 ID', key: 'sessionId', sortable: true, width: '100px' }, { title: '用户 ID', key: 'sessionId', sortable: true, width: '100px' },
], ],
}, },
{ title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' }, { title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' },
@@ -526,19 +530,6 @@ export default {
}); });
}, },
//
debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
},
// //
handleTableOptions(options) { handleTableOptions(options) {
// //
@@ -579,83 +570,93 @@ export default {
}, },
// //
async fetchConversations() { fetchConversations: (() => {
this.loading = true; let controller = new AbortController();
try {
//
const params = {
page: this.pagination.page,
page_size: this.pagination.page_size
};
// - combobox return async function () {
if (this.platformFilter.length > 0) { //
const platforms = this.platformFilter.map(item => controller?.abort()
typeof item === 'object' ? item.value : item controller = new AbortController();
);
params.platforms = platforms.join(',');
}
if (this.messageTypeFilter.length > 0) { this.loading = true;
params.message_types = this.messageTypeFilter.join(','); try {
} //
const params = {
page: this.pagination.page,
page_size: this.pagination.page_size
};
if (this.search) { // - combobox
params.search = this.search.trim(); if (this.platformFilter.length > 0) {
} const platforms = this.platformFilter.map(item =>
typeof item === 'object' ? item.value : item
// );
params.exclude_ids = 'astrbot'; params.platforms = platforms.join(',');
params.exclude_platforms = 'webchat';
const response = await axios.get('/api/conversation/list', { params });
this.lastAppliedFilters = { ...this.currentFilters }; //
if (response.data.status === "ok") {
const data = response.data.data;
if (!data || !data.conversations) {
console.error('API 返回数据格式不符合预期:', data);
this.showErrorMessage(this.tm('messages.fetchError'));
return;
} }
// sessionId if (this.messageTypeFilter.length > 0) {
this.conversations = (data.conversations || []).map(conv => { params.message_types = this.messageTypeFilter.join(',');
// }
conv.sessionInfo = this.parseSessionId(conv.user_id);
return conv; if (this.search) {
params.search = this.search.trim();
}
//
params.exclude_ids = 'astrbot';
params.exclude_platforms = 'webchat';
const response = await axios.get('/api/conversation/list', {
signal: controller.signal,
params
}); });
// this.lastAppliedFilters = { ...this.currentFilters }; //
if (data.pagination) {
this.pagination = { if (response.data.status === "ok") {
page: data.pagination.page || 1, const data = response.data.data;
page_size: data.pagination.page_size || 20,
total: data.pagination.total || 0, if (!data || !data.conversations) {
total_pages: data.pagination.total_pages || 1 console.error('API 返回数据格式不符合预期:', data);
}; this.showErrorMessage(this.tm('messages.fetchError'));
return;
}
// sessionId
this.conversations = (data.conversations || []).map(conv => {
//
conv.sessionInfo = this.parseSessionId(conv.user_id);
return conv;
});
//
if (data.pagination) {
this.pagination = {
page: data.pagination.page || 1,
page_size: data.pagination.page_size || 20,
total: data.pagination.total || 0,
total_pages: data.pagination.total_pages || 1
};
} else {
console.warn('API 响应中没有分页信息');
}
} else { } else {
console.warn('API 响应中没有分页信息'); this.showErrorMessage(response.data.message || this.tm('messages.fetchError'));
} }
} else { } catch (error) {
this.showErrorMessage(response.data.message || this.tm('messages.fetchError')); if (axios.isCancel(error)) return;
}
} catch (error) { console.error('获取对话列表出错:', error);
console.error('获取对话列表出错:', error); if (error.response) {
if (error.response) { console.error('错误响应数据:', error.response.data);
console.error('错误响应数据:', error.response.data); console.error('错误状态码:', error.response.status);
console.error('错误状态码:', error.response.status); }
} this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError'));
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError')); } finally {
} finally {
// this.loading = false;
setTimeout(() => {
this.loading = false; this.loading = false;
}, 200); }
} }
}, })(),
// //
async viewConversation(item) { async viewConversation(item) {
@@ -993,6 +994,14 @@ export default {
flex-direction: column; flex-direction: column;
} }
.text-truncate {
display: inline-block;
max-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 动画 */ /* 动画 */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
+28 -1
View File
@@ -5,6 +5,7 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import ReadmeDialog from '@/components/shared/ReadmeDialog.vue'; import ReadmeDialog from '@/components/shared/ReadmeDialog.vue';
import ProxySelector from '@/components/shared/ProxySelector.vue'; import ProxySelector from '@/components/shared/ProxySelector.vue';
import axios from 'axios'; import axios from 'axios';
import { pinyin } from 'pinyin-pro';
import { useCommonStore } from '@/stores/common'; import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables'; import { useI18n, useModuleI18n } from '@/i18n/composables';
@@ -65,6 +66,32 @@ const marketSearch = ref("");
const filterKeys = ['name', 'desc', 'author']; const filterKeys = ['name', 'desc', 'author'];
const refreshingMarket = ref(false); const refreshingMarket = ref(false);
//
const normalizeStr = (s) => (s ?? '').toString().toLowerCase().trim();
const toPinyinText = (s) => pinyin(s ?? '', { toneType: 'none' }).toLowerCase().replace(/\s+/g, '');
const toInitials = (s) => pinyin(s ?? '', { pattern: 'first', toneType: 'none' }).toLowerCase().replace(/\s+/g, '');
const marketCustomFilter = (value, query, item) => {
const q = normalizeStr(query);
if (!q) return true;
const candidates = new Set();
if (value != null) candidates.add(String(value));
if (item?.name) candidates.add(String(item.name));
if (item?.trimmedName) candidates.add(String(item.trimmedName));
if (item?.desc) candidates.add(String(item.desc));
if (item?.author) candidates.add(String(item.author));
for (const v of candidates) {
const nv = normalizeStr(v);
if (nv.includes(q)) return true;
const pv = toPinyinText(v);
if (pv.includes(q)) return true;
const iv = toInitials(v);
if (iv.includes(q)) return true;
}
return false;
};
const plugin_handler_info_headers = computed(() => [ const plugin_handler_info_headers = computed(() => [
{ title: tm('table.headers.eventType'), key: 'event_type_h' }, { title: tm('table.headers.eventType'), key: 'event_type_h' },
{ title: tm('table.headers.description'), key: 'desc', maxWidth: '250px' }, { title: tm('table.headers.description'), key: 'desc', maxWidth: '250px' },
@@ -772,7 +799,7 @@ onMounted(async () => {
<v-col cols="12" md="12" style="padding: 0px;"> <v-col cols="12" md="12" style="padding: 0px;">
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name" <v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
:loading="loading_" v-model:search="marketSearch" :filter-keys="filterKeys"> :loading="loading_" v-model:search="marketSearch" :filter-keys="filterKeys" :custom-filter="marketCustomFilter">
<template v-slot:item.name="{ item }"> <template v-slot:item.name="{ item }">
<div class="d-flex align-center" <div class="d-flex align-center"
style="overflow-x: auto; scrollbar-width: thin; scrollbar-track-color: transparent;"> style="overflow-x: auto; scrollbar-width: thin; scrollbar-track-color: transparent;">
@@ -345,6 +345,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import { debounce } from 'lodash'
import { useI18n, useModuleI18n } from '@/i18n/composables' import { useI18n, useModuleI18n } from '@/i18n/composables'
export default { export default {
@@ -953,10 +954,10 @@ export default {
}, },
// //
handleSearchChange() { handleSearchChange: debounce(function() {
this.currentPage = 1; // this.currentPage = 1; //
this.loadSessions(); this.loadSessions();
}, }, 300),
// //
handlePlatformChange() { handlePlatformChange() {
+2 -2
View File
@@ -601,11 +601,11 @@ export default {
checkPlugin() { checkPlugin() {
axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base') axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
.then(response => { .then(response => {
if (response.data.status !== 'ok') { if (response.data.status !== 'ok' || response.data.data.length === 0) {
this.showSnackbar(this.tm('messages.pluginNotAvailable'), 'error'); this.showSnackbar(this.tm('messages.pluginNotAvailable'), 'error');
return return
} }
if (!response.data.data.activated) { if (!response.data.data[0].activated) {
this.showSnackbar(this.tm('messages.pluginNotActivated'), 'error'); this.showSnackbar(this.tm('messages.pluginNotActivated'), 'error');
return return
} }
+24 -8
View File
@@ -6,6 +6,7 @@ from astrbot.core.platform.message_type import MessageType
from astrbot.core.provider.sources.dify_source import ProviderDify from astrbot.core.provider.sources.dify_source import ProviderDify
from astrbot.core.provider.sources.coze_source import ProviderCoze from astrbot.core.provider.sources.coze_source import ProviderCoze
from astrbot.api import sp, logger from astrbot.api import sp, logger
from ..long_term_memory import LongTermMemory
from typing import Union from typing import Union
from enum import Enum from enum import Enum
@@ -36,10 +37,21 @@ class RstScene(Enum):
class ConversationCommands: class ConversationCommands:
def __init__(self, context: star.Context, ltm=None): def __init__(self, context: star.Context, ltm: LongTermMemory | None = None):
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
@@ -254,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
) )
# 长期记忆 # 长期记忆
@@ -289,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(
@@ -433,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
@@ -41,7 +41,7 @@ class Main(star.Star):
self.tool_c = ToolCommands(self.context) self.tool_c = ToolCommands(self.context)
self.plugin_c = PluginCommands(self.context) self.plugin_c = PluginCommands(self.context)
self.admin_c = AdminCommands(self.context) self.admin_c = AdminCommands(self.context)
self.conversation_c = ConversationCommands(self.context) self.conversation_c = ConversationCommands(self.context, self.ltm)
self.provider_c = ProviderCommands(self.context) self.provider_c = ProviderCommands(self.context)
self.persona_c = PersonaCommands(self.context) self.persona_c = PersonaCommands(self.context)
self.alter_cmd_c = AlterCmdCommands(self.context) self.alter_cmd_c = AlterCmdCommands(self.context)
+4 -3
View File
@@ -205,13 +205,14 @@ class Main(star.Star):
return return
for comp in event.message_obj.message: for comp in event.message_obj.message:
if isinstance(comp, File): if isinstance(comp, File):
if comp.file.startswith("http"): file_path = await comp.get_file()
if file_path.startswith("http"):
name = comp.name if comp.name else uuid.uuid4().hex[:8] name = comp.name if comp.name else uuid.uuid4().hex[:8]
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, name) path = os.path.join(temp_dir, name)
await download_file(comp.file, path) await download_file(file_path, path)
else: else:
path = comp.file path = file_path
self.user_file_msg_buffer[event.get_session_id()].append(path) self.user_file_msg_buffer[event.get_session_id()].append(path)
logger.debug(f"User {uid} uploaded file: {path}") logger.debug(f"User {uid} uploaded file: {path}")
yield event.plain_result(f"代码执行器: 文件已经上传: {path}") yield event.plain_result(f"代码执行器: 文件已经上传: {path}")
+3 -3
View File
@@ -1,5 +1,5 @@
import os import os
from googlesearch import search from googlesearch.asearch import asearch
from . import SearchEngine, SearchResult from . import SearchEngine, SearchResult
@@ -14,14 +14,14 @@ class Google(SearchEngine):
async def search(self, query: str, num_results: int) -> List[SearchResult]: async def search(self, query: str, num_results: int) -> List[SearchResult]:
results = [] results = []
try: try:
ls = search( ls = asearch(
query, query,
advanced=True, advanced=True,
num_results=num_results, num_results=num_results,
timeout=3, timeout=3,
proxy=self.proxy, proxy=self.proxy,
) )
for i in ls: async for i in ls:
results.append( results.append(
SearchResult(title=i.title, url=i.url, snippet=i.description) SearchResult(title=i.title, url=i.url, snippet=i.description)
) )
+10 -5
View File
@@ -46,7 +46,11 @@ class Main(star.Star):
self.bing_search = Bing() self.bing_search = Bing()
self.sogo_search = Sogo() self.sogo_search = Sogo()
self.google = Google() self.google = None
try:
self.google = Google()
except Exception as e:
logger.error(f"google search init error: {e}, disable google search")
async def _tidy_text(self, text: str) -> str: async def _tidy_text(self, text: str) -> str:
"""清理文本,去除空格、换行符等""" """清理文本,去除空格、换行符等"""
@@ -89,10 +93,11 @@ class Main(star.Star):
self, query, num_results: int = 5 self, query, num_results: int = 5
) -> list[SearchResult]: ) -> list[SearchResult]:
results = [] results = []
try: if self.google:
results = await self.google.search(query, num_results) try:
except Exception as e: results = await self.google.search(query, num_results)
logger.error(f"google search error: {e}, try the next one...") except Exception as e:
logger.error(f"google search error: {e}, try the next one...")
if len(results) == 0: if len(results) == 0:
logger.debug("search google failed") logger.debug("search google failed")
try: try:
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "AstrBot" name = "AstrBot"
version = "4.3.0" version = "4.3.3"
description = "易上手的多平台 LLM 聊天机器人及开发框架" description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"