Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c6b6eef8c4 Complete Docker compatibility fix with enhanced documentation
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-08-16 16:03:54 +00:00
copilot-swe-agent[bot]
50cf263076 Implement CLI Docker compatibility fix and login-info command
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-08-16 16:01:36 +00:00
copilot-swe-agent[bot]
2554548088 Initial commit: fix formatting and explore codebase
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-08-16 15:53:37 +00:00
copilot-swe-agent[bot]
aa4a2d10e2 Initial plan 2025-08-16 15:48:10 +00:00
49 changed files with 375 additions and 506 deletions

View File

@@ -25,8 +25,6 @@ jobs:
echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
mkdir -p dashboard/dist/assets
echo $COMMIT_SHA > dashboard/dist/assets/version
cd dashboard
zip -r dist.zip dist
- name: Archive production artifacts
uses: actions/upload-artifact@v4
@@ -35,13 +33,3 @@ jobs:
path: |
dashboard/dist
!dist/**/*.md
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: release-${{ github.sha }}
owner: AstrBotDevs
repo: astrbot-release-harbour
body: "Automated release from commit ${{ github.sha }}"
token: ${{ secrets.ASTRBOT_HARBOUR_TOKEN }}
artifacts: "dashboard/dist.zip"

View File

@@ -1,4 +1,4 @@
<img width="430" height="31" alt="image" src="https://github.com/user-attachments/assets/474c822c-fab7-41be-8c23-6dae252823ed" /><p align="center">
<p align="center">
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
@@ -25,7 +25,7 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
</div>
AstrBot 是一个开源的一站式 Agentic 聊天机器人平台及开发框架。
AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型LLM接入功能的聊天机器人及开发框架。
## ✨ 主要功能

0
astrbot.lock Normal file
View File

View File

@@ -139,6 +139,14 @@ def conf():
- dashboard.password: Dashboard 密码
- callback_api_base: 回调接口基址
可用子命令:
- set: 设置配置项值
- get: 获取配置项值
- login-info: 显示 Web 管理面板登录信息
"""
pass
@@ -204,3 +212,44 @@ def get_config(key: str = None):
click.echo(f" {key}: {value}")
except (KeyError, TypeError):
pass
@conf.command(name="login-info")
def get_login_info():
"""显示 Web 管理面板的登录信息
在 Docker 环境中使用示例:
docker exec -e ASTRBOT_ROOT=/AstrBot astrbot-container astrbot conf login-info
"""
config = _load_config()
try:
username = _get_nested_item(config, "dashboard.username")
# 注意我们不显示实际的MD5哈希密码而是提示用户如何重置
click.echo("🔐 Web 管理面板登录信息:")
click.echo(f" 用户名: {username}")
click.echo(" 密码: [已加密存储]")
click.echo()
click.echo("💡 如需重置密码,请使用以下命令:")
click.echo(" astrbot conf set dashboard.password <新密码>")
click.echo()
click.echo("🌐 访问地址:")
# 尝试获取端口信息
try:
port = _get_nested_item(config, "dashboard.port")
click.echo(f" http://localhost:{port}")
click.echo(f" http://your-server-ip:{port}")
except (KeyError, TypeError):
click.echo(" http://localhost:6185 (默认端口)")
click.echo(" http://your-server-ip:6185 (默认端口)")
click.echo()
click.echo("📋 Docker 环境使用说明:")
click.echo(" 如果在 Docker 中运行,请使用以下命令格式:")
click.echo(" docker exec -e ASTRBOT_ROOT=/AstrBot <容器名> astrbot conf login-info")
except KeyError:
click.echo("❌ 无法找到登录配置,请先运行 'astrbot init' 初始化")
except Exception as e:
raise click.UsageError(f"获取登录信息失败: {str(e)}")

View File

@@ -16,7 +16,13 @@ def check_astrbot_root(path: str | Path) -> bool:
def get_astrbot_root() -> Path:
"""获取Astrbot根目录路径"""
return Path.cwd()
import os
# 使用与core应用相同的路径解析逻辑优先使用ASTRBOT_ROOT环境变量
if path := os.environ.get("ASTRBOT_ROOT"):
return Path(path)
else:
return Path.cwd()
async def check_dashboard(astrbot_root: Path) -> None:

View File

@@ -124,15 +124,17 @@ def build_plug_list(plugins_dir: Path) -> list:
if metadata and all(
k in metadata for k in ["name", "desc", "version", "author", "repo"]
):
result.append({
"name": str(metadata.get("name", "")),
"desc": str(metadata.get("desc", "")),
"version": str(metadata.get("version", "")),
"author": str(metadata.get("author", "")),
"repo": str(metadata.get("repo", "")),
"status": PluginStatus.INSTALLED,
"local_path": str(plugin_dir),
})
result.append(
{
"name": str(metadata.get("name", "")),
"desc": str(metadata.get("desc", "")),
"version": str(metadata.get("version", "")),
"author": str(metadata.get("author", "")),
"repo": str(metadata.get("repo", "")),
"status": PluginStatus.INSTALLED,
"local_path": str(plugin_dir),
}
)
# 获取在线插件列表
online_plugins = []
@@ -142,15 +144,17 @@ def build_plug_list(plugins_dir: Path) -> list:
resp.raise_for_status()
data = resp.json()
for plugin_id, plugin_info in data.items():
online_plugins.append({
"name": str(plugin_id),
"desc": str(plugin_info.get("desc", "")),
"version": str(plugin_info.get("version", "")),
"author": str(plugin_info.get("author", "")),
"repo": str(plugin_info.get("repo", "")),
"status": PluginStatus.NOT_INSTALLED,
"local_path": None,
})
online_plugins.append(
{
"name": str(plugin_id),
"desc": str(plugin_info.get("desc", "")),
"version": str(plugin_info.get("version", "")),
"author": str(plugin_info.get("author", "")),
"repo": str(plugin_info.get("repo", "")),
"status": PluginStatus.NOT_INSTALLED,
"local_path": None,
}
)
except Exception as e:
click.echo(f"获取在线插件列表失败: {e}", err=True)

View File

@@ -6,7 +6,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "3.5.26"
VERSION = "3.5.24"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
# 默认配置
@@ -65,7 +65,7 @@ DEFAULT_CONFIG = {
"show_tool_use_status": False,
"streaming_segmented": False,
"separate_provider": True,
"max_agent_step": 30
"max_agent_step": 30,
},
"provider_stt_settings": {
"enable": False,
@@ -103,7 +103,6 @@ DEFAULT_CONFIG = {
"t2i_endpoint": "",
"t2i_use_file_service": False,
"http_proxy": "",
"no_proxy": ["localhost", "127.0.0.1", "::1"],
"dashboard": {
"enable": True,
"username": "astrbot",
@@ -599,11 +598,8 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.openai.com/v1",
"timeout": 120,
"model_config": {
"model": "gpt-4o-mini",
"temperature": 0.4
},
"hint": "也兼容所有与OpenAI API兼容的服务。"
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"hint": "也兼容所有与OpenAI API兼容的服务。",
},
"Azure OpenAI": {
"id": "azure",
@@ -615,10 +611,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "",
"timeout": 120,
"model_config": {
"model": "gpt-4o-mini",
"temperature": 0.4
},
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
},
"xAI": {
"id": "xai",
@@ -629,10 +622,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.x.ai/v1",
"timeout": 120,
"model_config": {
"model": "grok-2-latest",
"temperature": 0.4
},
"model_config": {"model": "grok-2-latest", "temperature": 0.4},
},
"Anthropic": {
"hint": "注意Claude系列模型的温度调节范围为0到1.0,超出可能导致报错",
@@ -647,11 +637,11 @@ CONFIG_METADATA_2 = {
"model_config": {
"model": "claude-3-5-sonnet-latest",
"max_tokens": 4096,
"temperature": 0.2
"temperature": 0.2,
},
},
"Ollama": {
"hint":"启用前请确保已正确安装并运行 Ollama 服务端Ollama默认不带鉴权无需修改key",
"hint": "启用前请确保已正确安装并运行 Ollama 服务端Ollama默认不带鉴权无需修改key",
"id": "ollama_default",
"provider": "ollama",
"type": "openai_chat_completion",
@@ -659,10 +649,7 @@ CONFIG_METADATA_2 = {
"enable": True,
"key": ["ollama"], # ollama 的 key 默认是 ollama
"api_base": "http://localhost:11434/v1",
"model_config": {
"model": "llama3.1-8b",
"temperature": 0.4
},
"model_config": {"model": "llama3.1-8b", "temperature": 0.4},
},
"LM Studio": {
"id": "lm_studio",
@@ -687,7 +674,7 @@ CONFIG_METADATA_2 = {
"timeout": 120,
"model_config": {
"model": "gemini-1.5-flash",
"temperature": 0.4
"temperature": 0.4,
},
},
"Gemini": {
@@ -701,7 +688,7 @@ CONFIG_METADATA_2 = {
"timeout": 120,
"model_config": {
"model": "gemini-2.0-flash-exp",
"temperature": 0.4
"temperature": 0.4,
},
"gm_resp_image_modal": False,
"gm_native_search": False,
@@ -726,10 +713,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.deepseek.com/v1",
"timeout": 120,
"model_config": {
"model": "deepseek-chat",
"temperature": 0.4
},
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
},
"302.AI": {
"id": "302ai",
@@ -740,10 +724,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.302.ai/v1",
"timeout": 120,
"model_config": {
"model": "gpt-4.1-mini",
"temperature": 0.4
},
"model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
},
"硅基流动": {
"id": "siliconflow",
@@ -756,7 +737,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.siliconflow.cn/v1",
"model_config": {
"model": "deepseek-ai/DeepSeek-V3",
"temperature": 0.4
"temperature": 0.4,
},
},
"PPIO派欧云": {
@@ -770,7 +751,7 @@ CONFIG_METADATA_2 = {
"timeout": 120,
"model_config": {
"model": "deepseek/deepseek-r1",
"temperature": 0.4
"temperature": 0.4,
},
},
"优云智算": {
@@ -795,10 +776,7 @@ CONFIG_METADATA_2 = {
"key": [],
"timeout": 120,
"api_base": "https://api.moonshot.cn/v1",
"model_config": {
"model": "moonshot-v1-8k",
"temperature": 0.4
},
"model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
},
"智谱 AI": {
"id": "zhipu_default",
@@ -826,7 +804,7 @@ CONFIG_METADATA_2 = {
"dify_query_input_key": "astrbot_text_query",
"variables": {},
"timeout": 60,
"hint": "请确保你在 AstrBot 里设置的 APP 类型和 Dify 里面创建的应用的类型一致!"
"hint": "请确保你在 AstrBot 里设置的 APP 类型和 Dify 里面创建的应用的类型一致!",
},
"阿里云百炼应用": {
"id": "dashscope",
@@ -854,10 +832,7 @@ CONFIG_METADATA_2 = {
"key": [],
"timeout": 120,
"api_base": "https://api-inference.modelscope.cn/v1",
"model_config": {
"model": "Qwen/Qwen3-32B",
"temperature": 0.4
},
"model_config": {"model": "Qwen/Qwen3-32B", "temperature": 0.4},
},
"FastGPT": {
"id": "fastgpt",
@@ -972,7 +947,6 @@ CONFIG_METADATA_2 = {
"api_key": "",
"api_base": "https://api.fish.audio/v1",
"fishaudio-tts-character": "可莉",
"fishaudio-tts-reference-id": "",
"timeout": "20",
},
"阿里云百炼 TTS(API)": {
@@ -1566,11 +1540,6 @@ CONFIG_METADATA_2 = {
"type": "string",
"hint": "fishaudio TTS 的角色。默认为可莉。更多角色请访问https://fish.audio/zh-CN/discovery",
},
"fishaudio-tts-reference-id": {
"description": "reference_id",
"type": "string",
"hint": "fishaudio TTS 的参考模型ID可选。如果填入此字段将直接使用模型ID而不通过角色名称查询。例如626bb6d3f3364c9cbc3aa6a67300a664。更多模型请访问https://fish.audio/zh-CN/discovery进入模型详情界面后可复制模型ID",
},
"whisper_hint": {
"description": "本地部署 Whisper 模型须知",
"type": "string",
@@ -1917,12 +1886,6 @@ CONFIG_METADATA_2 = {
"type": "string",
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
},
"no_proxy": {
"description": "直连地址列表",
"type": "list",
"items": {"type": "string"},
"hint": "在此处添加不希望通过代理访问的地址,例如内部服务地址。回车添加,可添加多个,如未设置代理请忽略此配置",
},
"timezone": {
"description": "时区",
"type": "string",

View File

@@ -47,23 +47,12 @@ class AstrBotCoreLifecycle:
self.db = db # 初始化数据库
# 设置代理
proxy_config = self.astrbot_config.get("http_proxy", "")
if proxy_config != "":
os.environ["https_proxy"] = proxy_config
os.environ["http_proxy"] = proxy_config
logger.debug(f"Using proxy: {proxy_config}")
# 设置 no_proxy
no_proxy_list = self.astrbot_config.get("no_proxy", [])
os.environ["no_proxy"] = ",".join(no_proxy_list)
else:
# 清空代理环境变量
if "https_proxy" in os.environ:
del os.environ["https_proxy"]
if "http_proxy" in os.environ:
del os.environ["http_proxy"]
if "no_proxy" in os.environ:
del os.environ["no_proxy"]
logger.debug("HTTP proxy cleared")
if self.astrbot_config.get("http_proxy", ""):
os.environ["https_proxy"] = self.astrbot_config["http_proxy"]
os.environ["http_proxy"] = self.astrbot_config["http_proxy"]
if proxy := os.environ.get("https_proxy"):
logger.debug(f"Using proxy: {proxy}")
os.environ["no_proxy"] = "localhost"
async def initialize(self):
"""

View File

@@ -1,3 +1,3 @@
from .vec_db import FaissVecDB
__all__ = ["FaissVecDB"]
__all__ = ["FaissVecDB"]

View File

@@ -128,6 +128,7 @@ class Plain(BaseMessageComponent):
async def to_dict(self):
return {"type": "text", "data": {"text": self.text}}
class Face(BaseMessageComponent):
type: ComponentType = "Face"
id: int

View File

@@ -8,10 +8,11 @@ from enum import Enum, auto
class AgentState(Enum):
"""Agent 状态枚举"""
IDLE = auto() # 初始状态
RUNNING = auto() # 运行中
DONE = auto() # 完成
ERROR = auto() # 错误状态
IDLE = auto() # 初始状态
RUNNING = auto() # 运行中
DONE = auto() # 完成
ERROR = auto() # 错误状态
class AgentResponseData(T.TypedDict):

View File

@@ -335,10 +335,6 @@ class LLMRequestSubStage(Stage):
):
return
if not llm_response.completion_text and not req.tool_calls_result:
logger.debug("LLM 响应为空,不保存记录。")
return
# 历史上下文
messages = copy.deepcopy(req.contexts)
# 这一轮对话请求的用户输入

View File

@@ -144,6 +144,8 @@ class RespondStage(Stage):
try:
if await self._is_empty_message_chain(result.chain):
logger.info("消息为空,跳过发送阶段")
event.clear_result()
event.stop_event()
return
except Exception as e:
logger.warning(f"空内容检查异常: {e}")

View File

@@ -26,7 +26,7 @@ class DingtalkMessageEvent(AstrMessageEvent):
await asyncio.get_event_loop().run_in_executor(
None,
client.reply_markdown,
segment.text,
"AstrBot",
segment.text,
self.message_obj.raw_message,
)
@@ -57,6 +57,7 @@ class DingtalkMessageEvent(AstrMessageEvent):
logger.error(f"钉钉图片处理失败: {e}")
logger.warning(f"跳过图片发送: {image_path}")
continue
async def send(self, message: MessageChain):
await self.send_with_client(self.client, message)
await super().send(message)

View File

@@ -41,7 +41,8 @@ class DiscordBotClient(discord.Bot):
await self.on_ready_once_callback()
except Exception as e:
logger.error(
f"[Discord] on_ready_once_callback 执行失败: {e}", exc_info=True)
f"[Discord] on_ready_once_callback 执行失败: {e}", exc_info=True
)
def _create_message_data(self, message: discord.Message) -> dict:
"""从 discord.Message 创建数据字典"""
@@ -90,7 +91,6 @@ class DiscordBotClient(discord.Bot):
message_data = self._create_message_data(message)
await self.on_message_received(message_data)
def _extract_interaction_content(self, interaction: discord.Interaction) -> str:
"""从交互中提取内容"""
interaction_type = interaction.type

View File

@@ -79,9 +79,12 @@ class DiscordButton(BaseMessageComponent):
self.url = url
self.disabled = disabled
class DiscordReference(BaseMessageComponent):
"""Discord引用组件"""
type: str = "discord_reference"
def __init__(self, message_id: str, channel_id: str):
self.message_id = message_id
self.channel_id = channel_id
@@ -98,7 +101,6 @@ class DiscordView(BaseMessageComponent):
self.components = components or []
self.timeout = timeout
def to_discord_view(self) -> discord.ui.View:
"""转换为Discord View对象"""
view = discord.ui.View(timeout=self.timeout)

View File

@@ -53,7 +53,13 @@ class DiscordPlatformEvent(AstrMessageEvent):
# 解析消息链为 Discord 所需的对象
try:
content, files, view, embeds, reference_message_id = await self._parse_to_discord(message)
(
content,
files,
view,
embeds,
reference_message_id,
) = await self._parse_to_discord(message)
except Exception as e:
logger.error(f"[Discord] 解析消息链时失败: {e}", exc_info=True)
return
@@ -206,8 +212,7 @@ class DiscordPlatformEvent(AstrMessageEvent):
if await asyncio.to_thread(path.exists):
file_bytes = await asyncio.to_thread(path.read_bytes)
files.append(
discord.File(BytesIO(file_bytes),
filename=i.name)
discord.File(BytesIO(file_bytes), filename=i.name)
)
else:
logger.warning(

View File

@@ -3,23 +3,15 @@ import botpy.message
import botpy.types
import botpy.types.message
import asyncio
import base64
import aiofiles
from astrbot.core.utils.io import file_to_base64, download_image_by_url
from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
from astrbot.api.message_components import Plain, Image, Record
from astrbot.api.message_components import Plain, Image
from botpy import Client
from botpy.http import Route
from astrbot.api import logger
from botpy.types.message import Media
from botpy.types import message
from typing import Optional
import random
import uuid
import os
class QQOfficialMessageEvent(AstrMessageEvent):
@@ -44,7 +36,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
stream_payload = {"state": 1, "id": None, "index": 0, "reset": False}
last_edit_time = 0 # 上次编辑消息的时间
throttle_interval = 1 # 编辑消息的间隔时间 (秒)
ret = None
try:
async for chain in generator:
source = self.message_obj.raw_message
@@ -94,10 +85,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
plain_text,
image_base64,
image_path,
record_file_path
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
if not plain_text and not image_base64 and not image_path and not record_file_path:
if not plain_text and not image_base64 and not image_path:
return
payload = {
@@ -108,8 +98,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
if not isinstance(source, (botpy.message.Message, botpy.message.DirectMessage)):
payload["msg_seq"] = random.randint(1, 10000)
ret = None
match type(source):
case botpy.message.GroupMessage:
if image_base64:
@@ -118,12 +106,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
)
payload["media"] = media
payload["msg_type"] = 7
if record_file_path: # group record msg
media = await self.upload_group_and_c2c_record(
record_file_path, 3, group_openid=source.group_openid
)
payload["media"] = media
payload["msg_type"] = 7
ret = await self.bot.api.post_group_message(
group_openid=source.group_openid, **payload
)
@@ -134,12 +116,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
)
payload["media"] = media
payload["msg_type"] = 7
if record_file_path: # c2c record
media = await self.upload_group_and_c2c_record(
record_file_path, 3, openid = source.author.user_openid
)
payload["media"] = media
payload["msg_type"] = 7
if stream:
ret = await self.post_c2c_message(
openid=source.author.user_openid,
@@ -189,59 +165,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
)
return await self.bot.api._http.request(route, json=payload)
async def upload_group_and_c2c_record(
self,
file_source: str,
file_type: int,
srv_send_msg: bool = False,
**kwargs
) -> Optional[Media]:
"""
上传媒体文件
"""
# 构建基础payload
payload = {
"file_type": file_type,
"srv_send_msg": srv_send_msg
}
# 处理文件数据
if os.path.exists(file_source):
# 读取本地文件
async with aiofiles.open(file_source, 'rb') as f:
file_content = await f.read()
# use base64 encode
payload["file_data"] = base64.b64encode(file_content).decode('utf-8')
else:
# 使用URL
payload["url"] = file_source
# 添加接收者信息和确定路由
if "openid" in kwargs:
payload["openid"] = kwargs["openid"]
route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
elif "group_openid" in kwargs:
payload["group_openid"] =kwargs["group_openid"]
route = Route("POST", "/v2/groups/{group_openid}/files", group_openid=kwargs["group_openid"])
else:
return None
try:
# 使用底层HTTP请求
result = await self.bot.api._http.request(route, json=payload)
if result:
return Media(
file_uuid=result.get("file_uuid"),
file_info=result.get("file_info"),
ttl=result.get("ttl", 0),
file_id=result.get("id", "")
)
except Exception as e:
logger.error(f"上传请求错误: {e}")
return None
async def post_c2c_message(
self,
openid: str,
@@ -268,7 +191,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
plain_text = ""
image_base64 = None # only one img supported
image_file_path = None
record_file_path = None
for i in message.chain:
if isinstance(i, Plain):
plain_text += i.text
@@ -284,21 +206,6 @@ class QQOfficialMessageEvent(AstrMessageEvent):
else:
image_base64 = file_to_base64(i.file)
image_base64 = image_base64.removeprefix("base64://")
elif isinstance(i, Record):
if i.file:
record_wav_path = await i.convert_to_file_path() # wav 路径
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
record_tecent_silk_path = os.path.join(temp_dir, f"{uuid.uuid4()}.silk")
try:
duration = await wav_to_tencent_silk(record_wav_path, record_tecent_silk_path)
if duration > 0:
record_file_path = record_tecent_silk_path
else:
record_file_path = None
logger.error("转换音频格式时出错音频时长不大于0")
except Exception as e:
logger.error(f"处理语音时出错: {e}")
record_file_path = None
else:
logger.debug(f"qq_official 忽略 {i.type}")
return plain_text, image_base64, image_file_path, record_file_path
return plain_text, image_base64, image_file_path

View File

@@ -308,7 +308,9 @@ class SlackAdapter(Platform):
base64_content = base64.b64encode(content).decode("utf-8")
return base64_content
else:
logger.error(f"Failed to download slack file: {resp.status} {await resp.text()}")
logger.error(
f"Failed to download slack file: {resp.status} {await resp.text()}"
)
raise Exception(f"下载文件失败: {resp.status}")
async def run(self) -> Awaitable[Any]:

View File

@@ -75,7 +75,13 @@ class SlackMessageEvent(AstrMessageEvent):
"text": {"type": "mrkdwn", "text": "文件上传失败"},
}
file_url = response["files"][0]["permalink"]
return {"type": "section", "text": {"type": "mrkdwn", "text": f"文件: <{file_url}|{segment.name or '文件'}>"}}
return {
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"文件: <{file_url}|{segment.name or '文件'}>",
},
}
else:
return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}

View File

@@ -66,7 +66,9 @@ class TelegramPlatformEvent(AstrMessageEvent):
return chunks
@classmethod
async def send_with_client(cls, client: ExtBot, message: MessageChain, user_name: str):
async def send_with_client(
cls, client: ExtBot, message: MessageChain, user_name: str
):
image_path = None
has_reply = False

View File

@@ -1,5 +1,6 @@
import asyncio
class WebChatQueueMgr:
def __init__(self) -> None:
self.queues = {}
@@ -30,4 +31,5 @@ class WebChatQueueMgr:
"""Check if a queue exists for the given conversation ID"""
return conversation_id in self.queues
webchat_queue_mgr = WebChatQueueMgr()

View File

@@ -213,10 +213,10 @@ class WeChatPadProAdapter(Platform):
def _extract_auth_key(self, data):
"""Helper method to extract auth_key from response data."""
if isinstance(data, dict):
auth_keys = data.get("authKeys") # 新接口
auth_keys = data.get("authKeys") # 新接口
if isinstance(auth_keys, list) and auth_keys:
return auth_keys[0]
elif isinstance(data, list) and data: # 旧接口
elif isinstance(data, list) and data: # 旧接口
return data[0]
return None
@@ -234,7 +234,9 @@ class WeChatPadProAdapter(Platform):
try:
async with session.post(url, params=params, json=payload) as response:
if response.status != 200:
logger.error(f"生成授权码失败: {response.status}, {await response.text()}")
logger.error(
f"生成授权码失败: {response.status}, {await response.text()}"
)
return
response_data = await response.json()
@@ -245,7 +247,9 @@ class WeChatPadProAdapter(Platform):
if self.auth_key:
logger.info("成功获取授权码")
else:
logger.error(f"生成授权码成功但未找到授权码: {response_data}")
logger.error(
f"生成授权码成功但未找到授权码: {response_data}"
)
else:
logger.error(f"生成授权码失败: {response_data}")
except aiohttp.ClientConnectorError as e:

View File

@@ -48,7 +48,12 @@ class WeChatKF(BaseWeChatAPI):
注意可能会出现返回条数少于limit的情况需结合返回的has_more字段判断是否继续请求。
:return: 接口调用结果
"""
data = {"token": token, "cursor": cursor, "limit": limit, "open_kfid": open_kfid}
data = {
"token": token,
"cursor": cursor,
"limit": limit,
"open_kfid": open_kfid,
}
return self._post("kf/sync_msg", data=data)
def get_service_state(self, open_kfid, external_userid):
@@ -72,7 +77,9 @@ class WeChatKF(BaseWeChatAPI):
}
return self._post("kf/service_state/get", data=data)
def trans_service_state(self, open_kfid, external_userid, service_state, servicer_userid=""):
def trans_service_state(
self, open_kfid, external_userid, service_state, servicer_userid=""
):
"""
变更会话状态
@@ -180,7 +187,9 @@ class WeChatKF(BaseWeChatAPI):
"""
return self._get("kf/customer/get_upgrade_service_config")
def upgrade_service(self, open_kfid, external_userid, service_type, member=None, groupchat=None):
def upgrade_service(
self, open_kfid, external_userid, service_type, member=None, groupchat=None
):
"""
为客户升级为专员或客户群服务
@@ -246,7 +255,9 @@ class WeChatKF(BaseWeChatAPI):
data = {"open_kfid": open_kfid, "start_time": start_time, "end_time": end_time}
return self._post("kf/get_corp_statistic", data=data)
def get_servicer_statistic(self, start_time, end_time, open_kfid=None, servicer_userid=None):
def get_servicer_statistic(
self, start_time, end_time, open_kfid=None, servicer_userid=None
):
"""
获取「客户数据统计」接待人员明细数据

View File

@@ -26,6 +26,7 @@ from optionaldict import optionaldict
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatKFMessage(BaseWeChatAPI):
"""
发送微信客服消息
@@ -125,35 +126,55 @@ class WeChatKFMessage(BaseWeChatAPI):
msg={"msgtype": "news", "link": {"link": articles_data}},
)
def send_msgmenu(self, user_id, open_kfid, head_content, menu_list, tail_content, msgid=""):
def send_msgmenu(
self, user_id, open_kfid, head_content, menu_list, tail_content, msgid=""
):
return self.send(
user_id,
open_kfid,
msgid,
msg={
"msgtype": "msgmenu",
"msgmenu": {"head_content": head_content, "list": menu_list, "tail_content": tail_content},
"msgmenu": {
"head_content": head_content,
"list": menu_list,
"tail_content": tail_content,
},
},
)
def send_location(self, user_id, open_kfid, name, address, latitude, longitude, msgid=""):
def send_location(
self, user_id, open_kfid, name, address, latitude, longitude, msgid=""
):
return self.send(
user_id,
open_kfid,
msgid,
msg={
"msgtype": "location",
"msgmenu": {"name": name, "address": address, "latitude": latitude, "longitude": longitude},
"msgmenu": {
"name": name,
"address": address,
"latitude": latitude,
"longitude": longitude,
},
},
)
def send_miniprogram(self, user_id, open_kfid, appid, title, thumb_media_id, pagepath, msgid=""):
def send_miniprogram(
self, user_id, open_kfid, appid, title, thumb_media_id, pagepath, msgid=""
):
return self.send(
user_id,
open_kfid,
msgid,
msg={
"msgtype": "miniprogram",
"msgmenu": {"appid": appid, "title": title, "thumb_media_id": thumb_media_id, "pagepath": pagepath},
"msgmenu": {
"appid": appid,
"title": title,
"thumb_media_id": thumb_media_id,
"pagepath": pagepath,
},
},
)

View File

@@ -160,7 +160,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
self.wexin_event_workers[msg.id] = future
await self.convert_message(msg, future)
# I love shield so much!
result = await asyncio.wait_for(asyncio.shield(future), 60) # wait for 60s
result = await asyncio.wait_for(
asyncio.shield(future), 60
) # wait for 60s
logger.debug(f"Got future result: {result}")
self.wexin_event_workers.pop(msg.id, None)
return result # xml. see weixin_offacc_event.py

View File

@@ -150,7 +150,6 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
return
logger.info(f"微信公众平台上传语音返回: {response}")
if active_send_mode:
self.client.message.send_voice(
message_obj.sender.user_id,

View File

@@ -1,6 +1,5 @@
import os
import uuid
import re
import ormsgpack
from pydantic import BaseModel, conint
from httpx import AsyncClient
@@ -25,8 +24,8 @@ class ServeTTSRequest(BaseModel):
# 参考音频
references: list[ServeReferenceAudio] = []
# 参考模型 ID
# 例如 https://fish.audio/m/626bb6d3f3364c9cbc3aa6a67300a664/
# 其中reference_id为 626bb6d3f3364c9cbc3aa6a67300a664
# 例如 https://fish.audio/m/7f92f8afb8ec43bf81429cc1c9199cb1/
# 其中reference_id为 7f92f8afb8ec43bf81429cc1c9199cb1
reference_id: str | None = None
# 对中英文文本进行标准化,这可以提高数字的稳定性
normalize: bool = True
@@ -45,7 +44,6 @@ class ProviderFishAudioTTSAPI(TTSProvider):
) -> None:
super().__init__(provider_config, provider_settings)
self.chosen_api_key: str = provider_config.get("api_key", "")
self.reference_id: str = provider_config.get("fishaudio-tts-reference-id", "")
self.character: str = provider_config.get("fishaudio-tts-character", "可莉")
self.api_base: str = provider_config.get(
"api_base", "https://api.fish-audio.cn/v1"
@@ -83,43 +81,11 @@ class ProviderFishAudioTTSAPI(TTSProvider):
return item["_id"]
return None
def _validate_reference_id(self, reference_id: str) -> bool:
"""
验证reference_id格式是否有效
Args:
reference_id: 参考模型ID
Returns:
bool: ID是否有效
"""
if not reference_id or not reference_id.strip():
return False
# FishAudio的reference_id通常是32位十六进制字符串
# 例如: 626bb6d3f3364c9cbc3aa6a67300a664
pattern = r'^[a-fA-F0-9]{32}$'
return bool(re.match(pattern, reference_id.strip()))
async def _generate_request(self, text: str) -> dict:
# 向前兼容逻辑优先使用reference_id如果没有则使用角色名称查询
if self.reference_id and self.reference_id.strip():
# 验证reference_id格式
if not self._validate_reference_id(self.reference_id):
raise ValueError(
f"无效的FishAudio参考模型ID: '{self.reference_id}'. "
f"请确保ID是32位十六进制字符串例如: 626bb6d3f3364c9cbc3aa6a67300a664"
f"您可以从 https://fish.audio/zh-CN/discovery 获取有效的模型ID。"
)
reference_id = self.reference_id.strip()
else:
# 回退到原来的角色名称查询逻辑
reference_id = await self._get_reference_id_by_character(self.character)
return ServeTTSRequest(
text=text,
format="wav",
reference_id=reference_id,
reference_id=await self._get_reference_id_by_character(self.character),
)
async def get_audio(self, text: str) -> str:

View File

@@ -431,7 +431,6 @@ class ProviderGoogleGenAI(Provider):
continue
llm_response = LLMResponse("assistant")
llm_response.raw_completion = result
llm_response.result_chain = self._process_content_parts(result, llm_response)
return llm_response
@@ -482,7 +481,6 @@ class ProviderGoogleGenAI(Provider):
part.function_call for part in chunk.candidates[0].content.parts
):
llm_response = LLMResponse("assistant", is_chunk=False)
llm_response.raw_completion = chunk
llm_response.result_chain = self._process_content_parts(
chunk, llm_response
)
@@ -498,7 +496,6 @@ class ProviderGoogleGenAI(Provider):
# Process the final chunk for potential tool calls or other content
if chunk.candidates[0].content.parts:
final_response = LLMResponse("assistant", is_chunk=False)
final_response.raw_completion = chunk
final_response.result_chain = self._process_content_parts(
chunk, final_response
)

View File

@@ -99,13 +99,10 @@ class ProviderOpenAIOfficial(Provider):
for key in to_del:
del payloads[key]
model = payloads.get("model", "")
# 针对 qwen3 模型的特殊处理:非流式调用必须设置 enable_thinking=false
model = payloads.get("model", "")
if "qwen3" in model.lower():
extra_body["enable_thinking"] = False
# 针对 deepseek 模型的特殊处理deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
elif model == "deepseek-reasoner" and "tools" in payloads:
del payloads["tools"]
extra_body["enable_thinking"] = False
completion = await self.client.chat.completions.create(
**payloads, stream=False, extra_body=extra_body

View File

@@ -2,7 +2,12 @@ from asyncio import Queue
from typing import List, Union
from astrbot.core import sp
from astrbot.core.provider.provider import Provider, TTSProvider, STTProvider, EmbeddingProvider
from astrbot.core.provider.provider import (
Provider,
TTSProvider,
STTProvider,
EmbeddingProvider,
)
from astrbot.core.provider.entities import ProviderType
from astrbot.core.db import BaseDatabase
from astrbot.core.config.astrbot_config import AstrBotConfig

View File

@@ -113,8 +113,7 @@ class CommandGroupFilter(HandlerFilter):
+ self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
)
raise ValueError(
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n"
+ tree
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" + tree
)
# complete_command_names = [name + " " for name in complete_command_names]

View File

@@ -7,6 +7,7 @@ from .star import star_map
T = TypeVar("T", bound="StarHandlerMetadata")
class StarHandlerRegistry(Generic[T]):
def __init__(self):
self.star_handlers_map: Dict[str, StarHandlerMetadata] = {}
@@ -49,7 +50,8 @@ class StarHandlerRegistry(Generic[T]):
self, module_name: str
) -> List[StarHandlerMetadata]:
return [
handler for handler in self._handlers
handler
for handler in self._handlers
if handler.handler_module_path == module_name
]
@@ -67,6 +69,7 @@ class StarHandlerRegistry(Generic[T]):
def __len__(self):
return len(self._handlers)
star_handlers_registry = StarHandlerRegistry()

View File

@@ -809,11 +809,11 @@ class PluginManager:
if star_metadata.star_cls is None:
return
if '__del__' in star_metadata.star_cls_type.__dict__:
if "__del__" in star_metadata.star_cls_type.__dict__:
asyncio.get_event_loop().run_in_executor(
None, star_metadata.star_cls.__del__
)
elif 'terminate' in star_metadata.star_cls_type.__dict__:
elif "terminate" in star_metadata.star_cls_type.__dict__:
await star_metadata.star_cls.terminate()
async def turn_on_plugin(self, plugin_name: str):

View File

@@ -182,7 +182,9 @@ class StarTools:
plugin_name = metadata.name
data_dir = Path(os.path.join(get_astrbot_data_path(), "plugin_data", plugin_name))
data_dir = Path(
os.path.join(get_astrbot_data_path(), "plugin_data", plugin_name)
)
try:
data_dir.mkdir(parents=True, exist_ok=True)

View File

@@ -56,9 +56,7 @@ class AstrBotUpdator(RepoZipUpdator):
try:
if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli
if os.name == "nt":
args = [
f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]
]
args = [f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]]
else:
args = sys.argv[1:]
os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args)
@@ -89,6 +87,7 @@ class AstrBotUpdator(RepoZipUpdator):
file_url = update_data[0]["zipball_url"]
elif str(version).startswith("v"):
# 更新到指定版本
logger.info(f"正在更新到指定版本: {version}")
for data in update_data:
if data["tag_name"] == version:
file_url = data["zipball_url"]
@@ -97,8 +96,8 @@ class AstrBotUpdator(RepoZipUpdator):
else:
if len(str(version)) != 40:
raise Exception("commit hash 长度不正确,应为 40")
file_url = f"https://github.com/Soulter/AstrBot/archive/{version}.zip"
logger.info(f"准备更新至指定版本的 AstrBot Core: {version}")
logger.info(f"正在尝试更新到指定 commit: {version}")
file_url = "https://github.com/Soulter/AstrBot/archive/" + version + ".zip"
if proxy:
proxy = proxy.removesuffix("/")
@@ -106,7 +105,6 @@ class AstrBotUpdator(RepoZipUpdator):
try:
await download_file(file_url, "temp.zip")
logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...")
self.unzip_file("temp.zip", self.MAIN_PATH)
except BaseException as e:
raise e

View File

@@ -8,7 +8,6 @@ import base64
import zipfile
import uuid
import psutil
import logging
import certifi
@@ -17,8 +16,6 @@ from typing import Union
from PIL import Image
from .astrbot_path import get_astrbot_data_path
logger = logging.getLogger("astrbot")
def on_error(func, path, exc_info):
"""
@@ -215,50 +212,19 @@ async def get_dashboard_version():
return None
async def download_dashboard(
path: str | None = None,
extract_path: str = "data",
latest: bool = True,
version: str | None = None,
proxy: str | None = None,
):
async def download_dashboard(path: str = None, extract_path: str = "data"):
"""下载管理面板文件"""
if path is None:
path = os.path.join(get_astrbot_data_path(), "dashboard.zip")
if latest or len(str(version)) != 40:
logger.info("准备下载最新发行版本的 AstrBot WebUI")
ver_name = "latest" if latest else version
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
try:
await download_file(dashboard_release_url, path, show_progress=True)
except BaseException as _:
if latest:
dashboard_release_url = "https://github.com/Soulter/AstrBot/releases/latest/download/dist.zip"
else:
dashboard_release_url = f"https://github.com/Soulter/AstrBot/releases/download/{version}/dist.zip"
if proxy:
dashboard_release_url = f"{proxy}/{dashboard_release_url}"
await download_file(dashboard_release_url, path, show_progress=True)
else:
logger.info(f"准备下载指定版本的 AstrBot WebUI: {version}")
url = (
"https://api.github.com/repos/AstrBotDevs/astrbot-release-harbour/releases"
dashboard_release_url = "https://astrbot-registry.soulter.top/download/astrbot-dashboard/latest/dist.zip"
try:
await download_file(dashboard_release_url, path, show_progress=True)
except BaseException as _:
dashboard_release_url = (
"https://github.com/Soulter/AstrBot/releases/latest/download/dist.zip"
)
if proxy:
url = f"{proxy}/{url}"
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(url) as resp:
if resp.status == 200:
releases = await resp.json()
for release in releases:
if version in release["tag_name"]:
download_url = release["assets"][0]["browser_download_url"]
await download_file(download_url, path, show_progress=True)
else:
logger.warning(f"未找到指定的版本的 Dashboard 构建文件: {version}")
return
await download_file(dashboard_release_url, path, show_progress=True)
print("解压管理面板文件中...")
with zipfile.ZipFile(path, "r") as z:
z.extractall(extract_path)

View File

@@ -5,6 +5,7 @@ from .astrbot_path import get_astrbot_data_path
_VT = TypeVar("_VT")
class SharedPreferences:
def __init__(self, path=None):
if path is None:

View File

@@ -181,13 +181,14 @@ class RepoZipUpdator:
"""
os.makedirs(target_dir, exist_ok=True)
update_dir = ""
logger.info(f"解压文件: {zip_path}")
with zipfile.ZipFile(zip_path, "r") as z:
update_dir = z.namelist()[0]
z.extractall(target_dir)
logger.debug(f"解压文件完成: {zip_path}")
files = os.listdir(os.path.join(target_dir, update_dir))
for f in files:
logger.info(f"移动更新文件/目录: {f}")
if os.path.isdir(os.path.join(target_dir, update_dir, f)):
if os.path.exists(os.path.join(target_dir, f)):
shutil.rmtree(os.path.join(target_dir, f), onerror=on_error)
@@ -197,13 +198,13 @@ class RepoZipUpdator:
shutil.move(os.path.join(target_dir, update_dir, f), target_dir)
try:
logger.debug(
logger.info(
f"删除临时更新文件: {zip_path}{os.path.join(target_dir, update_dir)}"
)
shutil.rmtree(os.path.join(target_dir, update_dir), onerror=on_error)
os.remove(zip_path)
except BaseException:
logger.warning(
logger.warn(
f"删除更新文件失败,可以手动删除 {zip_path}{os.path.join(target_dir, update_dir)}"
)

View File

@@ -210,11 +210,16 @@ class ConfigRoute(Route):
response = await asyncio.wait_for(
provider.text_chat(prompt="REPLY `PONG` ONLY"), timeout=45.0
)
logger.debug(f"Received response from {status_info['name']}: {response}")
logger.debug(
f"Received response from {status_info['name']}: {response}"
)
if response is not None:
status_info["status"] = "available"
response_text_snippet = ""
if hasattr(response, "completion_text") and response.completion_text:
if (
hasattr(response, "completion_text")
and response.completion_text
):
response_text_snippet = (
response.completion_text[:70] + "..."
if len(response.completion_text) > 70
@@ -233,29 +238,48 @@ class ConfigRoute(Route):
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'"
)
else:
status_info["error"] = "Test call returned None, but expected an LLMResponse object."
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.")
status_info["error"] = (
"Test call returned None, but expected an LLMResponse object."
)
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None."
)
except asyncio.TimeoutError:
status_info["error"] = "Connection timed out after 45 seconds during test call."
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.")
status_info["error"] = (
"Connection timed out after 45 seconds during test call."
)
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) timed out."
)
except Exception as e:
error_message = str(e)
status_info["error"] = error_message
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}")
logger.debug(f"Traceback for {status_info['name']}:\n{traceback.format_exc()}")
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}"
)
logger.debug(
f"Traceback for {status_info['name']}:\n{traceback.format_exc()}"
)
elif provider_capability_type == ProviderType.EMBEDDING:
try:
# For embedding, we can call the get_embedding method with a short prompt.
embedding_result = await provider.get_embedding("health_check")
if isinstance(embedding_result, list) and (not embedding_result or isinstance(embedding_result[0], float)):
if isinstance(embedding_result, list) and (
not embedding_result or isinstance(embedding_result[0], float)
):
status_info["status"] = "available"
else:
status_info["status"] = "unavailable"
status_info["error"] = f"Embedding test failed: unexpected result type {type(embedding_result)}"
status_info["error"] = (
f"Embedding test failed: unexpected result type {type(embedding_result)}"
)
except Exception as e:
logger.error(f"Error testing embedding provider {provider_name}: {e}", exc_info=True)
logger.error(
f"Error testing embedding provider {provider_name}: {e}",
exc_info=True,
)
status_info["status"] = "unavailable"
status_info["error"] = f"Embedding test failed: {str(e)}"
@@ -267,41 +291,71 @@ class ConfigRoute(Route):
status_info["status"] = "available"
else:
status_info["status"] = "unavailable"
status_info["error"] = f"TTS test failed: unexpected result type {type(audio_result)}"
status_info["error"] = (
f"TTS test failed: unexpected result type {type(audio_result)}"
)
except Exception as e:
logger.error(f"Error testing TTS provider {provider_name}: {e}", exc_info=True)
logger.error(
f"Error testing TTS provider {provider_name}: {e}", exc_info=True
)
status_info["status"] = "unavailable"
status_info["error"] = f"TTS test failed: {str(e)}"
elif provider_capability_type == ProviderType.SPEECH_TO_TEXT:
try:
logger.debug(f"Sending health check audio to provider: {status_info['name']}")
sample_audio_path = os.path.join(get_astrbot_path(), "samples", "stt_health_check.wav")
logger.debug(
f"Sending health check audio to provider: {status_info['name']}"
)
sample_audio_path = os.path.join(
get_astrbot_path(), "samples", "stt_health_check.wav"
)
if not os.path.exists(sample_audio_path):
status_info["status"] = "unavailable"
status_info["error"] = "STT test failed: sample audio file not found."
logger.warning(f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}")
status_info["error"] = (
"STT test failed: sample audio file not found."
)
logger.warning(
f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}"
)
else:
text_result = await provider.get_text(sample_audio_path)
if isinstance(text_result, str) and text_result:
status_info["status"] = "available"
snippet = text_result[:70] + "..." if len(text_result) > 70 else text_result
logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'")
snippet = (
text_result[:70] + "..."
if len(text_result) > 70
else text_result
)
logger.info(
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'"
)
else:
status_info["status"] = "unavailable"
status_info["error"] = f"STT test failed: unexpected result type {type(text_result)}"
logger.warning(f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}")
status_info["error"] = (
f"STT test failed: unexpected result type {type(text_result)}"
)
logger.warning(
f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}"
)
except Exception as e:
logger.error(f"Error testing STT provider {provider_name}: {e}", exc_info=True)
logger.error(
f"Error testing STT provider {provider_name}: {e}", exc_info=True
)
status_info["status"] = "unavailable"
status_info["error"] = f"STT test failed: {str(e)}"
else:
logger.debug(f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}")
logger.debug(
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}"
)
status_info["status"] = "available"
status_info["error"] = "This provider type is not tested and is assumed to be available."
status_info["error"] = (
"This provider type is not tested and is assumed to be available."
)
return status_info
def _error_response(self, message: str, status_code: int = 500, log_fn=logger.error):
def _error_response(
self, message: str, status_code: int = 500, log_fn=logger.error
):
log_fn(message)
# 记录更详细的traceback信息但只在是严重错误时
if status_code == 500:
@@ -312,7 +366,9 @@ class ConfigRoute(Route):
"""API: check a single LLM Provider's status by id"""
provider_id = request.args.get("id")
if not provider_id:
return self._error_response("Missing provider_id parameter", 400, logger.warning)
return self._error_response(
"Missing provider_id parameter", 400, logger.warning
)
logger.info(f"API call: /config/provider/check_one id={provider_id}")
try:
@@ -320,16 +376,21 @@ class ConfigRoute(Route):
target = prov_mgr.inst_map.get(provider_id)
if not target:
logger.warning(f"Provider with id '{provider_id}' not found in provider_manager.")
return Response().error(f"Provider with id '{provider_id}' not found").__dict__
logger.warning(
f"Provider with id '{provider_id}' not found in provider_manager."
)
return (
Response()
.error(f"Provider with id '{provider_id}' not found")
.__dict__
)
result = await self._test_single_provider(target)
return Response().ok(result).__dict__
except Exception as e:
return self._error_response(
f"Critical error checking provider {provider_id}: {e}",
500
f"Critical error checking provider {provider_id}: {e}", 500
)
async def get_configs(self):

View File

@@ -10,7 +10,9 @@ class LogRoute(Route):
super().__init__(context)
self.log_broker = log_broker
self.app.add_url_rule("/api/live-log", view_func=self.log, methods=["GET"])
self.app.add_url_rule("/api/log-history", view_func=self.log_history, methods=["GET"])
self.app.add_url_rule(
"/api/log-history", view_func=self.log_history, methods=["GET"]
)
async def log(self):
async def stream():
@@ -48,9 +50,15 @@ class LogRoute(Route):
"""获取日志历史"""
try:
logs = list(self.log_broker.log_cache)
return Response().ok(data={
"logs": logs,
}).__dict__
return (
Response()
.ok(
data={
"logs": logs,
}
)
.__dict__
)
except BaseException as e:
logger.error(f"获取日志历史失败: {e}")
return Response().error(f"获取日志历史失败: {e}").__dict__

View File

@@ -48,7 +48,7 @@ class UpdateRoute(Route):
"version": f"v{VERSION}",
"has_new_version": ret is not None,
"dashboard_version": dv,
"dashboard_has_new_version": dv and dv != f"v{VERSION}",
"dashboard_has_new_version": dv != f"v{VERSION}",
},
).__dict__
except Exception as e:
@@ -82,12 +82,11 @@ class UpdateRoute(Route):
latest=latest, version=version, proxy=proxy
)
try:
await download_dashboard(
latest=latest, version=version, proxy=proxy
)
except Exception as e:
logger.error(f"下载管理面板文件失败: {e}")
if latest:
try:
await download_dashboard()
except Exception as e:
logger.error(f"下载管理面板文件失败: {e}")
# pip 更新依赖
logger.info("更新依赖中...")

View File

@@ -1,13 +0,0 @@
# What's Changed
1. 修复: 修复插件可能存在的无法正常禁用的问题 ([#2352](https://github.com/Soulter/AstrBot/issues/2352))
2. ❗修复:当返回文本为空并且存在函数调用时错误地被终止事件,导致函数调用结果未被正常返回 ([#2491](https://github.com/Soulter/AstrBot/issues/2491))
3. 修复:修复无法清空 AstrBot 配置下的 http_proxy 代理的问题 ([#2434](https://github.com/Soulter/AstrBot/issues/2434))
4. ❗修复Gemini 下开启流式输出时,持久化的消息结果不完整 ([#2424](https://github.com/Soulter/AstrBot/issues/2424))
5. 修复:注册文件时由于 file:/// 前缀,导致文件被误判为不存在的问题 ([#2325](https://github.com/Soulter/AstrBot/issues/2325))
6. 优化: 为部分类型供应商添加默认的温度选项 ([#2321](https://github.com/Soulter/AstrBot/issues/2321))
7. 优化: 适配 Qwen3 模型非流式输出下需要传入 enable_think 参数(否则报错) ([#2424](https://github.com/Soulter/AstrBot/issues/2424))
8. 优化:支持配置工具调用轮数上限,默认 30
9. 新增: 添加 WebUI 语义化预发布版本提醒和检测功能
> 新版本预告: v4.0.0 即将发布。

View File

@@ -1,9 +0,0 @@
# What's Changed
1. 新增:为 FishAudio TTS 添加可选的 reference_id 直接指定功能 ([#2513](https://github.com/AstrBotDevs/AstrBot/issues/2513))
2. 新增Gemini 添加对 LLMResponse 的 raw_completion 支持
3. 新增:支持官方 QQ 接口发送语音 ([#2525](https://github.com/AstrBotDevs/AstrBot/issues/2525))
4. 新增:调用 deepseek-reasoner 时自动移除 tools ([#2531](https://github.com/AstrBotDevs/AstrBot/issues/2531))
5. 新增:添加 no_proxy 配置支持以优化代理设置 ([#2564](https://github.com/AstrBotDevs/AstrBot/issues/2564))
6. 新增:支持升级的同时更新到指定版本的 WebUI
7. 修复: 修复编辑会话名称窗口的圆角和左右边距问题 ([#2583](https://github.com/AstrBotDevs/AstrBot/issues/2583))

View File

@@ -25,15 +25,9 @@
"dev": "🧐 Development (master branch)"
},
"updateToLatest": "Update to Latest Version",
"preRelease": "Pre-release",
"preReleaseWarning": {
"title": "Pre-release Version Notice",
"description": "Versions marked as pre-release may contain unknown issues or bugs and are not recommended for production use. If you encounter any problems, please visit ",
"issueLink": "GitHub Issues"
},
"tip": "💡 TIP:",
"tipLink": "",
"tipContinue": "By default, the corresponding version of the WebUI files will be downloaded when switching versions. The WebUI code is located in the dashboard directory of the project, and you can use npm to build it yourself.",
"tip": "💡 TIP: Switching to an older version or a specific version will not re-download the dashboard files, which may cause some data display errors. You can find the corresponding dashboard files dist.zip at",
"tipLink": "here",
"tipContinue": ", extract and replace the data/dist folder. Of course, the frontend source code is in the dashboard directory, you can also build it yourself using npm install and npm build.",
"dockerTip": "The `Update to Latest Version` button will try to update both the bot main program and the dashboard. If you are using Docker deployment, you can also re-pull the image or use",
"dockerTipLink": "watchtower",
"dockerTipContinue": "to automatically monitor and pull.",

View File

@@ -25,14 +25,9 @@
"dev": "🧐 开发版(master 分支)"
},
"updateToLatest": "更新到最新版本",
"preRelease": "预发布",
"preReleaseWarning": {
"title": "预发布版本提醒",
"description": "标有预发布标签的版本可能存在未知问题或 Bug不建议在生产环境使用。如发现问题请提交至 ",
"issueLink": "GitHub Issues"
},
"tip": "💡 TIP: ",
"tipContinue": "默认在切换版本时会下载对应版本的 WebUI 文件。WebUI 代码位于项目的 dashboard 目录,您可使用 npm 自行构建。",
"tip": "💡 TIP: 跳到旧版本或者切换到某个版本不会重新下载管理面板文件,这可能会造成部分数据显示错误。您可在",
"tipLink": "此处",
"tipContinue": "找到对应的面板文件 dist.zip解压后替换 data/dist 文件夹即可。当然,前端源代码在 dashboard 目录下,你也可以自己使用 npm install 和 npm build 构建。",
"dockerTip": "`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用",
"dockerTipLink": "watchtower",
"dockerTipContinue": "来自动监控拉取。",

View File

@@ -36,7 +36,7 @@ let dashboardHasNewVersion = ref(false);
let dashboardCurrentVersion = ref('');
let version = ref('');
let releases = ref([]);
let devCommits = ref<{ sha: string; date: string; message: string }[]>([]);
let devCommits = ref([]);
let updatingDashboardLoading = ref(false);
let installLoading = ref(false);
@@ -76,13 +76,6 @@ const open = (link: string) => {
window.open(link, '_blank');
};
// 检测是否为预发布版本
const isPreRelease = (version: string) => {
const preReleaseKeywords = ['alpha', 'beta', 'rc', 'pre', 'preview', 'dev'];
const lowerVersion = version.toLowerCase();
return preReleaseKeywords.some(keyword => lowerVersion.includes(keyword));
};
// 账户修改
function accountEdit() {
accountEditStatus.value.loading = true;
@@ -189,46 +182,23 @@ function getReleases() {
}
function getDevCommits() {
let proxy = localStorage.getItem('selectedGitHubProxy') || '';
const originalUrl = "https://api.github.com/repos/Soulter/AstrBot/commits";
let commits_url = originalUrl;
if (proxy !== '') {
proxy = proxy.endsWith('/') ? proxy : proxy + '/';
commits_url = proxy + originalUrl;
}
function fetchCommits(url: string, onError?: () => void) {
fetch(url, {
headers: {
'Host': 'api.github.com',
'Referer': 'https://api.github.com'
}
})
fetch('https://api.github.com/repos/Soulter/AstrBot/commits', {
headers: {
'Host': 'api.github.com',
'Referer': 'https://api.github.com'
}
})
.then(response => response.json())
.then(data => {
devCommits.value = Array.isArray(data)
? data.map((commit: any) => ({
sha: commit.sha,
date: new Date(commit.commit.author.date).toLocaleString(),
message: commit.commit.message
}))
: [];
devCommits.value = data.map((commit: any) => ({
sha: commit.sha,
date: new Date(commit.commit.author.date).toLocaleString(),
message: commit.commit.message
}));
})
.catch(err => {
if (onError) {
onError();
} else {
console.log('获取开发版提交信息失败:', err);
}
console.log(err);
});
}
fetchCommits(commits_url, () => {
if (proxy !== '' && commits_url !== originalUrl) {
console.log('使用代理请求失败,尝试直接请求');
fetchCommits(originalUrl);
}
});
}
function switchVersion(version: string) {
@@ -335,7 +305,7 @@ commonStore.getStartTime();
</v-btn>
<!-- 更新对话框 -->
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1200'" :fullscreen="$vuetify.display.xs">
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1000'" :fullscreen="$vuetify.display.xs">
<template v-slot:activator="{ props }">
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
@@ -365,7 +335,8 @@ commonStore.getStartTime();
</div>
<div class="mb-4 mt-4">
<small>{{ t('core.header.updateDialog.tip') }}
<small>{{ t('core.header.updateDialog.tip') }} <a
href="https://github.com/Soulter/AstrBot/releases">{{ t('core.header.updateDialog.tipLink') }}</a>
{{ t('core.header.updateDialog.tipContinue') }}</small>
</div>
@@ -386,44 +357,11 @@ commonStore.getStartTime();
href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
</div>
<v-alert
v-if="releases.some(item => isPreRelease(item['tag_name']))"
type="warning"
variant="tonal"
border="start"
>
<template v-slot:prepend>
<v-icon>mdi-alert-circle-outline</v-icon>
</template>
<div class="text-body-2">
<strong>{{ t('core.header.updateDialog.preReleaseWarning.title') }}</strong>
<br>
{{ t('core.header.updateDialog.preReleaseWarning.description') }}
<a href="https://github.com/Soulter/AstrBot/issues" target="_blank" class="text-decoration-none">
{{ t('core.header.updateDialog.preReleaseWarning.issueLink') }}
</a>
</div>
</v-alert>
<v-data-table :headers="releasesHeader" :items="releases" item-key="name" :items-per-page="5">
<template v-slot:item.tag_name="{ item }: { item: { tag_name: string } }">
<div class="d-flex align-center">
<span>{{ item.tag_name }}</span>
<v-chip
v-if="isPreRelease(item.tag_name)"
size="x-small"
color="warning"
variant="tonal"
class="ml-2"
>
{{ t('core.header.updateDialog.preRelease') }}
</v-chip>
</div>
</template>
<v-data-table :headers="releasesHeader" :items="releases" item-key="name">
<template v-slot:item.body="{ item }: { item: { body: string } }">
<v-tooltip :text="item.body">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="x-small">{{ t('core.header.updateDialog.table.view') }}</v-btn>
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="small">{{ t('core.header.updateDialog.table.view') }}</v-btn>
</template>
</v-tooltip>
</template>

View File

@@ -480,7 +480,7 @@
<!-- 会话命名编辑对话框 -->
<v-dialog v-model="nameEditDialog" max-width="500" min-height="60%">
<v-card v-if="selectedSessionForName">
<v-card v-if="selectedSessionForName" rounded="12">
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
<v-icon color="white" class="me-2">mdi-rename-box</v-icon>
<span>{{ tm('nameEditor.title') }}</span>
@@ -490,9 +490,8 @@
</v-btn>
</v-card-title>
<v-card-text>
<div style="padding-left: 16px; padding-right: 16px;">
<v-text-field
<v-card-text class="pa-4">
<v-text-field
v-model="newSessionName"
:label="tm('nameEditor.customName')"
:placeholder="tm('nameEditor.placeholder')"
@@ -519,7 +518,6 @@
>
{{ tm('nameEditor.hint') }}
</v-alert>
</div>
</v-card-text>
<v-card-actions class="px-4 pb-4">

View File

@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "3.5.26"
version = "3.5.24"
description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md"
requires-python = ">=3.10"