Compare commits

..

1 Commits

33 changed files with 1727 additions and 2579 deletions
+2 -29
View File
@@ -27,33 +27,6 @@ jobs:
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: git checkout ${{ steps.get-latest-tag.outputs.latest_tag }} run: git checkout ${{ steps.get-latest-tag.outputs.latest_tag }}
- name: Check if version is pre-release
id: check-prerelease
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
version="${{ steps.get-latest-tag.outputs.latest_tag }}"
else
version="${{ github.ref_name }}"
fi
if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "Version $version is a pre-release, will not push latest tag"
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
echo "Version $version is a stable release, will push latest tag"
fi
- name: Build Dashboard
run: |
cd dashboard
npm install
npm run build
mkdir -p dist/assets
echo $(git rev-parse HEAD) > dist/assets/version
cd ..
mkdir -p data
cp -r dashboard/dist data/
- name: Set QEMU - name: Set QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@@ -80,9 +53,9 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
${{ steps.check-prerelease.outputs.is_prerelease == 'false' && format('{0}/astrbot:latest', secrets.DOCKER_HUB_USERNAME) || '' }} ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }} ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
${{ steps.check-prerelease.outputs.is_prerelease == 'false' && 'ghcr.io/soulter/astrbot:latest' || '' }} ghcr.io/soulter/astrbot:latest
ghcr.io/soulter/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }} ghcr.io/soulter/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
- name: Post build notifications - name: Post build notifications
+3 -12
View File
@@ -37,10 +37,7 @@ async def check_dashboard(astrbot_root: Path) -> None:
): ):
click.echo("正在安装管理面板...") click.echo("正在安装管理面板...")
await download_dashboard( await download_dashboard(
path="data/dashboard.zip", path="data/dashboard.zip", extract_path=str(astrbot_root)
extract_path=str(astrbot_root),
version=f"v{VERSION}",
latest=False,
) )
click.echo("管理面板安装完成") click.echo("管理面板安装完成")
@@ -53,10 +50,7 @@ async def check_dashboard(astrbot_root: Path) -> None:
version = dashboard_version.split("v")[1] version = dashboard_version.split("v")[1]
click.echo(f"管理面板版本: {version}") click.echo(f"管理面板版本: {version}")
await download_dashboard( await download_dashboard(
path="data/dashboard.zip", path="data/dashboard.zip", extract_path=str(astrbot_root)
extract_path=str(astrbot_root),
version=f"v{VERSION}",
latest=False,
) )
except Exception as e: except Exception as e:
click.echo(f"下载管理面板失败: {e}") click.echo(f"下载管理面板失败: {e}")
@@ -65,10 +59,7 @@ async def check_dashboard(astrbot_root: Path) -> None:
click.echo("初始化管理面板目录...") click.echo("初始化管理面板目录...")
try: try:
await download_dashboard( await download_dashboard(
path=str(astrbot_root / "dashboard.zip"), path=str(astrbot_root / "dashboard.zip"), extract_path=str(astrbot_root)
extract_path=str(astrbot_root),
version=f"v{VERSION}",
latest=False,
) )
click.echo("管理面板初始化完成") click.echo("管理面板初始化完成")
except Exception as e: except Exception as e:
+9 -16
View File
@@ -36,21 +36,13 @@ class AstrBotConfigManager:
self.confs: dict[str, AstrBotConfig] = {} self.confs: dict[str, AstrBotConfig] = {}
"""uuid / "default" -> AstrBotConfig""" """uuid / "default" -> AstrBotConfig"""
self.confs["default"] = default_config self.confs["default"] = default_config
self.abconf_data = None
self._load_all_configs() self._load_all_configs()
def _get_abconf_data(self) -> dict:
"""获取所有的 abconf 数据"""
if self.abconf_data is None:
self.abconf_data = self.sp.get(
"abconf_mapping", {}, scope="global", scope_id="global"
)
return self.abconf_data
def _load_all_configs(self): def _load_all_configs(self):
"""Load all configurations from the shared preferences.""" """Load all configurations from the shared preferences."""
abconf_data = self._get_abconf_data() abconf_data = self.sp.get(
self.abconf_data = abconf_data "abconf_mapping", {}, scope="global", scope_id="global"
)
for uuid_, meta in abconf_data.items(): for uuid_, meta in abconf_data.items():
filename = meta["path"] filename = meta["path"]
conf_path = os.path.join(get_astrbot_config_path(), filename) conf_path = os.path.join(get_astrbot_config_path(), filename)
@@ -80,7 +72,9 @@ class AstrBotConfigManager:
ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型 ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
""" """
# uuid -> { "umop": list, "path": str, "name": str } # uuid -> { "umop": list, "path": str, "name": str }
abconf_data = self._get_abconf_data() abconf_data = self.sp.get(
"abconf_mapping", {}, scope="global", scope_id="global"
)
if isinstance(umo, MessageSession): if isinstance(umo, MessageSession):
umo = str(umo) umo = str(umo)
else: else:
@@ -121,7 +115,6 @@ class AstrBotConfigManager:
"name": random_word, "name": random_word,
} }
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
self.abconf_data = abconf_data
def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig: def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig:
"""获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。""" """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
@@ -154,7 +147,9 @@ class AstrBotConfigManager:
"""获取所有配置文件的元数据列表""" """获取所有配置文件的元数据列表"""
conf_list = [] conf_list = []
conf_list.append(DEFAULT_CONFIG_CONF_INFO) conf_list.append(DEFAULT_CONFIG_CONF_INFO)
abconf_mapping = self._get_abconf_data() abconf_mapping = self.sp.get(
"abconf_mapping", {}, scope="global", scope_id="global"
)
for uuid_, meta in abconf_mapping.items(): for uuid_, meta in abconf_mapping.items():
conf_list.append(ConfInfo(**meta, id=uuid_)) conf_list.append(ConfInfo(**meta, id=uuid_))
return conf_list return conf_list
@@ -223,7 +218,6 @@ class AstrBotConfigManager:
# 从映射中移除 # 从映射中移除
del abconf_data[conf_id] del abconf_data[conf_id]
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
self.abconf_data = abconf_data
logger.info(f"成功删除配置文件 {conf_id}") logger.info(f"成功删除配置文件 {conf_id}")
return True return True
@@ -269,7 +263,6 @@ class AstrBotConfigManager:
# 保存更新 # 保存更新
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
self.abconf_data = abconf_data
logger.info(f"成功更新配置文件 {conf_id} 的信息") logger.info(f"成功更新配置文件 {conf_id} 的信息")
return True return True
+35 -17
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.0.0-beta.5" VERSION = "4.0.0"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
# 默认配置 # 默认配置
@@ -51,6 +51,10 @@ DEFAULT_CONFIG = {
"enable": True, "enable": True,
"default_provider_id": "", "default_provider_id": "",
"default_image_caption_provider_id": "", "default_image_caption_provider_id": "",
"default_summarize_provider_id": "",
"context_exceed_calc_method": "token_size",
"max_token_size": 128000,
"max_context_length": 100,
"image_caption_prompt": "Please describe the image using Chinese.", "image_caption_prompt": "Please describe the image using Chinese.",
"provider_pool": ["*"], # "*" 表示使用所有可用的提供者 "provider_pool": ["*"], # "*" 表示使用所有可用的提供者
"wake_prefix": "", "wake_prefix": "",
@@ -64,7 +68,6 @@ DEFAULT_CONFIG = {
"default_personality": "default", "default_personality": "default",
"persona_pool": ["*"], "persona_pool": ["*"],
"prompt_prefix": "", "prompt_prefix": "",
"max_context_length": -1,
"dequeue_context_length": 1, "dequeue_context_length": 1,
"streaming_response": False, "streaming_response": False,
"show_tool_use_status": False, "show_tool_use_status": False,
@@ -866,9 +869,6 @@ CONFIG_METADATA_2 = {
"provider_type": "text_to_speech", "provider_type": "text_to_speech",
"enable": False, "enable": False,
"edge-tts-voice": "zh-CN-XiaoxiaoNeural", "edge-tts-voice": "zh-CN-XiaoxiaoNeural",
"rate": "+0%",
"volume": "+0%",
"pitch": "+0Hz",
"timeout": 20, "timeout": 20,
}, },
"GSV TTS(本地加载)": { "GSV TTS(本地加载)": {
@@ -1835,6 +1835,12 @@ CONFIG_METADATA_3 = {
"_special": "select_provider", "_special": "select_provider",
"hint": "留空时使用第一个模型。", "hint": "留空时使用第一个模型。",
}, },
"provider_settings.default_summarize_provider_id": {
"description": "默认对话总结模型",
"type": "string",
"_special": "select_provider",
"hint": "留空代表不进行对话总结。可用于压缩上下文以减少 token 用量,并一定程度上保持历史聊天记忆。",
},
"provider_settings.default_image_caption_provider_id": { "provider_settings.default_image_caption_provider_id": {
"description": "默认图片转述模型", "description": "默认图片转述模型",
"type": "string", "type": "string",
@@ -1853,6 +1859,28 @@ CONFIG_METADATA_3 = {
"hint": "留空代表不使用。", "hint": "留空代表不使用。",
"_special": "select_provider_tts", "_special": "select_provider_tts",
}, },
"provider_settings.context_exceed_calc_method": {
"description": "上下文超限的触发策略",
"type": "string",
"options": ["token_size", "context_length"],
"labels": ["基于 Token 长度(估算)", "基于对话轮数"],
"hint": "如配置了对话总结模型,则触发时总结对话内容,否则丢弃最旧部分。"
},
"provider_settings.max_context_length": {
"description": "对话轮数上限",
"type": "int",
"condition": {
"provider_settings.context_exceed_calc_method": "context_length"
}
},
"provider_settings.max_token_size": {
"description": "Token 长度上限(估算)",
"type": "int",
"hint": "超出这个数量时丢弃最旧的部分。",
"condition": {
"provider_settings.context_exceed_calc_method": "token_size"
}
},
"provider_settings.image_caption_prompt": { "provider_settings.image_caption_prompt": {
"description": "图片转述提示词", "description": "图片转述提示词",
"type": "text", "type": "text",
@@ -1916,7 +1944,7 @@ CONFIG_METADATA_3 = {
"type": "bool", "type": "bool",
}, },
"provider_settings.identifier": { "provider_settings.identifier": {
"description": "用户识别", "description": "用户感知",
"type": "bool", "type": "bool",
}, },
"provider_settings.datetime_system_prompt": { "provider_settings.datetime_system_prompt": {
@@ -1939,11 +1967,6 @@ CONFIG_METADATA_3 = {
"description": "不支持流式回复的平台采取分段输出", "description": "不支持流式回复的平台采取分段输出",
"type": "bool", "type": "bool",
}, },
"provider_settings.max_context_length": {
"description": "最多携带对话轮数",
"type": "int",
"hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条。-1 为不限制。",
},
"provider_settings.dequeue_context_length": { "provider_settings.dequeue_context_length": {
"description": "丢弃对话轮数", "description": "丢弃对话轮数",
"type": "int", "type": "int",
@@ -2291,7 +2314,7 @@ CONFIG_METADATA_3_SYSTEM = {
"condition": { "condition": {
"t2i_strategy": "remote", "t2i_strategy": "remote",
}, },
"_special": "t2i_template", "_special": "t2i_template"
}, },
"log_level": { "log_level": {
"description": "控制台日志级别", "description": "控制台日志级别",
@@ -2324,11 +2347,6 @@ CONFIG_METADATA_3_SYSTEM = {
"type": "string", "type": "string",
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`", "hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
}, },
"no_proxy": {
"description": "直连地址列表",
"type": "list",
"items": {"type": "string"},
},
}, },
} }
}, },
@@ -299,9 +299,7 @@ class LLMRequestSubStage(Stage):
self.max_context_length - 1, self.max_context_length - 1,
) )
self.streaming_response: bool = settings["streaming_response"] self.streaming_response: bool = settings["streaming_response"]
self.max_step: int = settings.get("max_agent_step", 30) self.max_step: int = settings.get("max_agent_step", 10)
if isinstance(self.max_step, bool): # workaround: #2622
self.max_step = 30
self.show_tool_use: bool = settings.get("show_tool_use_status", True) self.show_tool_use: bool = settings.get("show_tool_use_status", True)
for bwp in self.bot_wake_prefixs: for bwp in self.bot_wake_prefixs:
@@ -436,9 +434,7 @@ class LLMRequestSubStage(Stage):
provider_cfg = provider.provider_config.get("modalities", ["tool_use"]) provider_cfg = provider.provider_config.get("modalities", ["tool_use"])
# 如果模型不支持工具使用,但请求中包含工具列表,则清空。 # 如果模型不支持工具使用,但请求中包含工具列表,则清空。
if "tool_use" not in provider_cfg: if "tool_use" not in provider_cfg:
logger.debug( logger.debug(f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。")
f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。"
)
req.func_tool = None req.func_tool = None
# 插件可用性设置 # 插件可用性设置
if event.plugins_name is not None and req.func_tool: if event.plugins_name is not None and req.func_tool:
@@ -67,19 +67,12 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
session_id: str, session_id: str,
messages: list[dict], messages: list[dict],
): ):
# session_id 必须是纯数字字符串 if event:
session_id = int(session_id) if session_id.isdigit() else None
if is_group and isinstance(session_id, int):
await bot.send_group_msg(group_id=session_id, message=messages)
elif not is_group and isinstance(session_id, int):
await bot.send_private_msg(user_id=session_id, message=messages)
elif isinstance(event, Event): # 最后兜底
await bot.send(event=event, message=messages) await bot.send(event=event, message=messages)
elif is_group:
await bot.send_group_msg(group_id=session_id, message=messages)
else: else:
raise ValueError( await bot.send_private_msg(user_id=session_id, message=messages)
f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})"
)
@classmethod @classmethod
async def send_message( async def send_message(
@@ -90,15 +83,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
is_group: bool = False, is_group: bool = False,
session_id: str = None, session_id: str = None,
): ):
"""发送消息至 QQ 协议端(aiocqhttp)。 """发送消息"""
Args:
bot (CQHttp): aiocqhttp 机器人实例
message_chain (MessageChain): 要发送的消息链
event (Event | None, optional): aiocqhttp 事件对象.
is_group (bool, optional): 是否为群消息.
session_id (str | None, optional): 会话 ID(群号或 QQ 号
"""
# 转发消息、文件消息不能和普通消息混在一起发送 # 转发消息、文件消息不能和普通消息混在一起发送
send_one_by_one = any( send_one_by_one = any(
@@ -137,15 +122,18 @@ class AiocqhttpMessageEvent(AstrMessageEvent):
async def send(self, message: MessageChain): async def send(self, message: MessageChain):
"""发送消息""" """发送消息"""
event = getattr(self.message_obj, "raw_message", None) event = self.message_obj.raw_message
assert isinstance(event, Event), "Event must be an instance of aiocqhttp.Event"
is_group = bool(self.get_group_id()) is_group = False
session_id = self.get_group_id() if is_group else self.get_sender_id() if self.get_group_id():
is_group = True
session_id = self.get_group_id()
else:
session_id = self.get_sender_id()
await self.send_message( await self.send_message(
bot=self.bot, bot=self.bot,
message_chain=message, message_chain=message,
event=event, # 不强制要求一定是 Event event=event,
is_group=is_group, is_group=is_group,
session_id=session_id, session_id=session_id,
) )
+15 -17
View File
@@ -4,11 +4,9 @@ import json
from astrbot.core.utils.io import download_image_by_url from astrbot.core.utils.io import download_image_by_url
from astrbot import logger from astrbot import logger
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Dict, Type, Any from typing import List, Dict, Type
from astrbot.core.agent.tool import ToolSet from astrbot.core.agent.tool import ToolSet
from openai.types.chat.chat_completion import ChatCompletion from openai.types.chat.chat_completion import ChatCompletion
from google.genai.types import GenerateContentResponse
from anthropic.types import Message
from openai.types.chat.chat_completion_message_tool_call import ( from openai.types.chat.chat_completion_message_tool_call import (
ChatCompletionMessageToolCall, ChatCompletionMessageToolCall,
) )
@@ -32,11 +30,11 @@ class ProviderMetaData:
desc: str = "" desc: str = ""
"""提供商适配器描述.""" """提供商适配器描述."""
provider_type: ProviderType = ProviderType.CHAT_COMPLETION provider_type: ProviderType = ProviderType.CHAT_COMPLETION
cls_type: Type | None = None cls_type: Type = None
default_config_tmpl: dict | None = None default_config_tmpl: dict = None
"""平台的默认配置模板""" """平台的默认配置模板"""
provider_display_name: str | None = None provider_display_name: str = None
"""显示在 WebUI 配置页中的提供商名称,如空则是 type""" """显示在 WebUI 配置页中的提供商名称,如空则是 type"""
@@ -60,7 +58,7 @@ class ToolCallMessageSegment:
class AssistantMessageSegment: class AssistantMessageSegment:
"""OpenAI 格式的上下文中 role 为 assistant 的消息段。参考: https://platform.openai.com/docs/guides/function-calling""" """OpenAI 格式的上下文中 role 为 assistant 的消息段。参考: https://platform.openai.com/docs/guides/function-calling"""
content: str | None = None content: str = None
tool_calls: List[ChatCompletionMessageToolCall | Dict] = field(default_factory=list) tool_calls: List[ChatCompletionMessageToolCall | Dict] = field(default_factory=list)
role: str = "assistant" role: str = "assistant"
@@ -207,17 +205,17 @@ class ProviderRequest:
class LLMResponse: class LLMResponse:
role: str role: str
"""角色, assistant, tool, err""" """角色, assistant, tool, err"""
result_chain: MessageChain | None = None result_chain: MessageChain = None
"""返回的消息链""" """返回的消息链"""
tools_call_args: List[Dict[str, Any]] = field(default_factory=list) tools_call_args: List[Dict[str, any]] = field(default_factory=list)
"""工具调用参数""" """工具调用参数"""
tools_call_name: List[str] = field(default_factory=list) tools_call_name: List[str] = field(default_factory=list)
"""工具调用名称""" """工具调用名称"""
tools_call_ids: List[str] = field(default_factory=list) tools_call_ids: List[str] = field(default_factory=list)
"""工具调用 ID""" """工具调用 ID"""
raw_completion: ChatCompletion | GenerateContentResponse | Message | None = None raw_completion: ChatCompletion = None
_new_record: Dict[str, Any] | None = None _new_record: Dict[str, any] = None
_completion_text: str = "" _completion_text: str = ""
@@ -228,12 +226,12 @@ class LLMResponse:
self, self,
role: str, role: str,
completion_text: str = "", completion_text: str = "",
result_chain: MessageChain | None = None, result_chain: MessageChain = None,
tools_call_args: List[Dict[str, Any]] | None = None, tools_call_args: List[Dict[str, any]] = None,
tools_call_name: List[str] | None = None, tools_call_name: List[str] = None,
tools_call_ids: List[str] | None = None, tools_call_ids: List[str] = None,
raw_completion: ChatCompletion | None = None, raw_completion: ChatCompletion = None,
_new_record: Dict[str, Any] | None = None, _new_record: Dict[str, any] = None,
is_chunk: bool = False, is_chunk: bool = False,
): ):
"""初始化 LLMResponse """初始化 LLMResponse
+18 -57
View File
@@ -15,7 +15,7 @@ from astrbot import logger
from astrbot.api.provider import Provider from astrbot.api.provider import Provider
from astrbot.core.message.message_event_result import MessageChain from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.provider.entities import LLMResponse from astrbot.core.provider.entities import LLMResponse
from astrbot.core.provider.func_tool_manager import ToolSet from astrbot.core.provider.func_tool_manager import FuncCall
from astrbot.core.utils.io import download_image_by_url from astrbot.core.utils.io import download_image_by_url
from ..register import register_provider_adapter from ..register import register_provider_adapter
@@ -61,7 +61,7 @@ class ProviderGoogleGenAI(Provider):
default_persona, default_persona,
) )
self.api_keys: list = provider_config.get("key", []) self.api_keys: list = provider_config.get("key", [])
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 None
self.timeout: int = int(provider_config.get("timeout", 180)) self.timeout: int = int(provider_config.get("timeout", 180))
self.api_base: Optional[str] = provider_config.get("api_base", None) self.api_base: Optional[str] = provider_config.get("api_base", None)
@@ -96,9 +96,6 @@ class ProviderGoogleGenAI(Provider):
async def _handle_api_error(self, e: APIError, keys: list[str]) -> bool: async def _handle_api_error(self, e: APIError, keys: list[str]) -> bool:
"""处理API错误,返回是否需要重试""" """处理API错误,返回是否需要重试"""
if e.message is None:
e.message = ""
if e.code == 429 or "API key not valid" in e.message: if e.code == 429 or "API key not valid" in e.message:
keys.remove(self.chosen_api_key) keys.remove(self.chosen_api_key)
if len(keys) > 0: if len(keys) > 0:
@@ -122,7 +119,7 @@ class ProviderGoogleGenAI(Provider):
async def _prepare_query_config( async def _prepare_query_config(
self, self,
payloads: dict, payloads: dict,
tools: Optional[ToolSet] = None, tools: Optional[FuncCall] = None,
system_instruction: Optional[str] = None, system_instruction: Optional[str] = None,
modalities: Optional[list[str]] = None, modalities: Optional[list[str]] = None,
temperature: float = 0.7, temperature: float = 0.7,
@@ -324,15 +321,11 @@ class ProviderGoogleGenAI(Provider):
@staticmethod @staticmethod
def _process_content_parts( def _process_content_parts(
candidate: types.Candidate, llm_response: LLMResponse result: types.GenerateContentResponse, llm_response: LLMResponse
) -> MessageChain: ) -> MessageChain:
"""处理内容部分并构建消息链""" """处理内容部分并构建消息链"""
if not candidate.content: finish_reason = result.candidates[0].finish_reason
logger.warning(f"收到的 candidate.content 为空: {candidate}") result_parts: Optional[types.Part] = result.candidates[0].content.parts
raise Exception("API 返回的 candidate.content 为空。")
finish_reason = candidate.finish_reason
result_parts: list[types.Part] | None = candidate.content.parts
if finish_reason == types.FinishReason.SAFETY: if finish_reason == types.FinishReason.SAFETY:
raise Exception("模型生成内容未通过 Gemini 平台的安全检查") raise Exception("模型生成内容未通过 Gemini 平台的安全检查")
@@ -350,28 +343,22 @@ class ProviderGoogleGenAI(Provider):
raise Exception("模型生成内容违反 Gemini 平台政策") raise Exception("模型生成内容违反 Gemini 平台政策")
if not result_parts: if not result_parts:
logger.warning(f"收到的 candidate.content.parts 为空: {candidate}") logger.debug(result.candidates)
raise Exception("API 返回的 candidate.content.parts 为空。") raise Exception("API 返回的内容为空。")
chain = [] chain = []
part: types.Part part: types.Part
# 暂时这样Fallback # 暂时这样Fallback
if all( if all(
part.inline_data part.inline_data and part.inline_data.mime_type.startswith("image/")
and part.inline_data.mime_type
and part.inline_data.mime_type.startswith("image/")
for part in result_parts for part in result_parts
): ):
chain.append(Comp.Plain("这是图片")) chain.append(Comp.Plain("这是图片"))
for part in result_parts: for part in result_parts:
if part.text: if part.text:
chain.append(Comp.Plain(part.text)) chain.append(Comp.Plain(part.text))
elif ( elif part.function_call:
part.function_call
and part.function_call.name is not None
and part.function_call.args is not None
):
llm_response.role = "tool" llm_response.role = "tool"
llm_response.tools_call_name.append(part.function_call.name) llm_response.tools_call_name.append(part.function_call.name)
llm_response.tools_call_args.append(part.function_call.args) llm_response.tools_call_args.append(part.function_call.args)
@@ -379,16 +366,11 @@ class ProviderGoogleGenAI(Provider):
llm_response.tools_call_ids.append( llm_response.tools_call_ids.append(
part.function_call.id or part.function_call.name part.function_call.id or part.function_call.name
) )
elif ( elif part.inline_data and part.inline_data.mime_type.startswith("image/"):
part.inline_data
and part.inline_data.mime_type
and part.inline_data.mime_type.startswith("image/")
and part.inline_data.data
):
chain.append(Comp.Image.fromBytes(part.inline_data.data)) chain.append(Comp.Image.fromBytes(part.inline_data.data))
return MessageChain(chain=chain) return MessageChain(chain=chain)
async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
"""非流式请求 Gemini API""" """非流式请求 Gemini API"""
system_instruction = next( system_instruction = next(
(msg["content"] for msg in payloads["messages"] if msg["role"] == "system"), (msg["content"] for msg in payloads["messages"] if msg["role"] == "system"),
@@ -414,10 +396,6 @@ class ProviderGoogleGenAI(Provider):
config=config, config=config,
) )
if not result.candidates:
logger.error(f"请求失败, 返回的 candidates 为空: {result}")
raise Exception("请求失败, 返回的 candidates 为空。")
if result.candidates[0].finish_reason == types.FinishReason.RECITATION: if result.candidates[0].finish_reason == types.FinishReason.RECITATION:
if temperature > 2: if temperature > 2:
raise Exception("温度参数已超过最大值2,仍然发生recitation") raise Exception("温度参数已超过最大值2,仍然发生recitation")
@@ -430,8 +408,6 @@ class ProviderGoogleGenAI(Provider):
break break
except APIError as e: except APIError as e:
if e.message is None:
e.message = ""
if "Developer instruction is not enabled" in e.message: if "Developer instruction is not enabled" in e.message:
logger.warning( logger.warning(
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)" f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
@@ -456,13 +432,11 @@ class ProviderGoogleGenAI(Provider):
llm_response = LLMResponse("assistant") llm_response = LLMResponse("assistant")
llm_response.raw_completion = result llm_response.raw_completion = result
llm_response.result_chain = self._process_content_parts( llm_response.result_chain = self._process_content_parts(result, llm_response)
result.candidates[0], llm_response
)
return llm_response return llm_response
async def _query_stream( async def _query_stream(
self, payloads: dict, tools: ToolSet | None self, payloads: dict, tools: FuncCall
) -> AsyncGenerator[LLMResponse, None]: ) -> AsyncGenerator[LLMResponse, None]:
"""流式请求 Gemini API""" """流式请求 Gemini API"""
system_instruction = next( system_instruction = next(
@@ -485,8 +459,6 @@ class ProviderGoogleGenAI(Provider):
) )
break break
except APIError as e: except APIError as e:
if e.message is None:
e.message = ""
if "Developer instruction is not enabled" in e.message: if "Developer instruction is not enabled" in e.message:
logger.warning( logger.warning(
f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)" f"{self.get_model()} 不支持 system prompt,已自动去除(影响人格设置)"
@@ -506,20 +478,13 @@ class ProviderGoogleGenAI(Provider):
async for chunk in result: async for chunk in result:
llm_response = LLMResponse("assistant", is_chunk=True) llm_response = LLMResponse("assistant", is_chunk=True)
if not chunk.candidates:
logger.warning(f"收到的 chunk 中 candidates 为空: {chunk}")
continue
if not chunk.candidates[0].content:
logger.warning(f"收到的 chunk 中 content 为空: {chunk}")
continue
if chunk.candidates[0].content.parts and any( if chunk.candidates[0].content.parts and any(
part.function_call for part in chunk.candidates[0].content.parts part.function_call for part in chunk.candidates[0].content.parts
): ):
llm_response = LLMResponse("assistant", is_chunk=False) llm_response = LLMResponse("assistant", is_chunk=False)
llm_response.raw_completion = chunk llm_response.raw_completion = chunk
llm_response.result_chain = self._process_content_parts( llm_response.result_chain = self._process_content_parts(
chunk.candidates[0], llm_response chunk, llm_response
) )
yield llm_response yield llm_response
return return
@@ -535,7 +500,7 @@ class ProviderGoogleGenAI(Provider):
final_response = LLMResponse("assistant", is_chunk=False) final_response = LLMResponse("assistant", is_chunk=False)
final_response.raw_completion = chunk final_response.raw_completion = chunk
final_response.result_chain = self._process_content_parts( final_response.result_chain = self._process_content_parts(
chunk.candidates[0], final_response chunk, final_response
) )
break break
@@ -601,8 +566,6 @@ class ProviderGoogleGenAI(Provider):
continue continue
break break
raise Exception("请求失败。")
async def text_chat_stream( async def text_chat_stream(
self, self,
prompt, prompt,
@@ -658,9 +621,7 @@ class ProviderGoogleGenAI(Provider):
return [ return [
m.name.replace("models/", "") m.name.replace("models/", "")
for m in models for m in models
if m.supported_actions if "generateContent" in m.supported_actions
and "generateContent" in m.supported_actions
and m.name
] ]
except APIError as e: except APIError as e:
raise Exception(f"获取模型列表失败: {e.message}") raise Exception(f"获取模型列表失败: {e.message}")
@@ -675,7 +636,7 @@ class ProviderGoogleGenAI(Provider):
self.chosen_api_key = key self.chosen_api_key = key
self._init_client() self._init_client()
async def assemble_context(self, text: str, image_urls: list[str] | None = None): async def assemble_context(self, text: str, image_urls: list[str] = None):
""" """
组装上下文。 组装上下文。
""" """
@@ -100,8 +100,8 @@ class ProviderOpenAIOfficial(Provider):
del payloads[key] del payloads[key]
model = payloads.get("model", "") model = payloads.get("model", "")
# 针对 qwen3 非 thinking 模型的特殊处理:非流式调用必须设置 enable_thinking=false # 针对 qwen3 模型的特殊处理:非流式调用必须设置 enable_thinking=false
if "qwen3" in model.lower() and "thinking" not in model.lower(): if "qwen3" in model.lower():
extra_body["enable_thinking"] = False extra_body["enable_thinking"] = False
# 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat # 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
elif model == "deepseek-reasoner" and "tools" in payloads: elif model == "deepseek-reasoner" and "tools" in payloads:
+5 -7
View File
@@ -56,7 +56,9 @@ class AstrBotUpdator(RepoZipUpdator):
try: try:
if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli
if os.name == "nt": 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: else:
args = sys.argv[1:] args = sys.argv[1:]
os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args) os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args)
@@ -66,13 +68,9 @@ class AstrBotUpdator(RepoZipUpdator):
logger.error(f"重启失败({py}, {e}),请尝试手动重启。") logger.error(f"重启失败({py}, {e}),请尝试手动重启。")
raise e raise e
async def check_update( async def check_update(self, url: str, current_version: str) -> ReleaseInfo:
self, url: str, current_version: str, consider_prerelease: bool = True
) -> ReleaseInfo:
"""检查更新""" """检查更新"""
return await super().check_update( return await super().check_update(self.ASTRBOT_RELEASE_API, VERSION)
self.ASTRBOT_RELEASE_API, VERSION, consider_prerelease
)
async def get_releases(self) -> list: async def get_releases(self) -> list:
return await self.fetch_release_info(self.ASTRBOT_RELEASE_API) return await self.fetch_release_info(self.ASTRBOT_RELEASE_API)
+1 -1
View File
@@ -227,7 +227,7 @@ async def download_dashboard(
path = os.path.join(get_astrbot_data_path(), "dashboard.zip") path = os.path.join(get_astrbot_data_path(), "dashboard.zip")
if latest or len(str(version)) != 40: if latest or len(str(version)) != 40:
logger.info(f"准备下载 {version} 发行版本的 AstrBot WebUI 文件") logger.info("准备下载最新发行版本的 AstrBot WebUI")
ver_name = "latest" if latest else version ver_name = "latest" if latest else version
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip" dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
try: try:
+4 -26
View File
@@ -107,38 +107,16 @@ class RepoZipUpdator:
"""Semver 版本比较""" """Semver 版本比较"""
return VersionComparator.compare_version(v1, v2) return VersionComparator.compare_version(v1, v2)
async def check_update( async def check_update(self, url: str, current_version: str) -> ReleaseInfo | None:
self, url: str, current_version: str, consider_prerelease: bool = True
) -> ReleaseInfo | None:
update_data = await self.fetch_release_info(url) update_data = await self.fetch_release_info(url)
tag_name = update_data[0]["tag_name"]
sel_release_data = None
if consider_prerelease:
tag_name = update_data[0]["tag_name"]
sel_release_data = update_data[0]
else:
for data in update_data:
# 跳过带有 alpha、beta 等预发布标签的版本
if re.search(
r"[\-_.]?(alpha|beta|rc|dev)[\-_.]?\d*$",
data["tag_name"],
re.IGNORECASE,
):
continue
tag_name = data["tag_name"]
sel_release_data = data
break
if not sel_release_data or not tag_name:
logger.error("未找到合适的发布版本")
return None
if self.compare_version(current_version, tag_name) >= 0: if self.compare_version(current_version, tag_name) >= 0:
return None return None
return ReleaseInfo( return ReleaseInfo(
version=tag_name, version=tag_name,
published_at=sel_release_data["published_at"], published_at=update_data[0]["published_at"],
body=f"{tag_name}\n\n{sel_release_data['body']}", body=update_data[0]["body"],
) )
async def download_from_repo_url(self, target_path: str, repo_url: str, proxy=""): async def download_from_repo_url(self, target_path: str, repo_url: str, proxy=""):
-14
View File
@@ -18,7 +18,6 @@ from astrbot.core.provider.register import provider_registry
from astrbot.core.star.star import star_registry from astrbot.core.star.star import star_registry
from astrbot.core import logger, html_renderer from astrbot.core import logger, html_renderer
from astrbot.core.provider import Provider from astrbot.core.provider import Provider
from astrbot.core.provider.provider import RerankProvider
import asyncio import asyncio
from astrbot.core.utils.t2i.network_strategy import CUSTOM_T2I_TEMPLATE_PATH from astrbot.core.utils.t2i.network_strategy import CUSTOM_T2I_TEMPLATE_PATH
@@ -482,19 +481,6 @@ class ConfigRoute(Route):
) )
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = f"STT test failed: {str(e)}" status_info["error"] = f"STT test failed: {str(e)}"
elif provider_capability_type == ProviderType.RERANK:
try:
assert isinstance(provider, RerankProvider)
await provider.rerank("Apple", documents=["apple", "banana"])
status_info["status"] = "available"
except Exception as e:
logger.error(
f"Error testing rerank provider {provider_name}: {e}",
exc_info=True,
)
status_info["status"] = "unavailable"
status_info["error"] = f"Rerank test failed: {str(e)}"
else: else:
logger.debug( logger.debug(
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}" f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}"
+5 -3
View File
@@ -57,7 +57,7 @@ class UpdateRoute(Route):
.__dict__ .__dict__
) )
else: else:
ret = await self.astrbot_updator.check_update(None, None, False) ret = await self.astrbot_updator.check_update(None, None)
return Response( return Response(
status="success", status="success",
message=str(ret) if ret is not None else "已经是最新版本了。", message=str(ret) if ret is not None else "已经是最新版本了。",
@@ -100,7 +100,9 @@ class UpdateRoute(Route):
) )
try: try:
await download_dashboard(latest=latest, version=version, proxy=proxy) await download_dashboard(
latest=latest, version=version, proxy=proxy
)
except Exception as e: except Exception as e:
logger.error(f"下载管理面板文件失败: {e}") logger.error(f"下载管理面板文件失败: {e}")
@@ -131,7 +133,7 @@ class UpdateRoute(Route):
async def update_dashboard(self): async def update_dashboard(self):
try: try:
try: try:
await download_dashboard(version=f"v{VERSION}", latest=False) await download_dashboard()
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__
-5
View File
@@ -1,5 +0,0 @@
# What's Changed
1. 修复:构建 docker 镜像时同时构建 webui,并放入镜像中。
2. 修复:下载 WebUI 文件时,明确版本号,以防止 latest 不一致导致下载的 WebUI 文件版本号与实际所需不符的问题。
3. 优化:优化版本检测,考虑预发布版本,移除 `更新到最新版本` 按钮
-3
View File
@@ -1,3 +0,0 @@
# What's Changed
> 请仔细阅读:**这是 v4.0.0 的测试版本(beta.3),功能尚未完全稳定和加入**。v4.0.0 被设计为向前兼容,如有任何插件兼容性问题或者其他异常请在 GitHub 提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues)。在测试版本期间,您可以无缝回退到旧版本的 AstrBot,并且数据不受影响。新版本文档请[从此](https://docs-v4.astrbot.app/)访问,直到第一个 v4.0.0 稳定版本发布。
-8
View File
@@ -1,8 +0,0 @@
# What's Changed
> 请仔细阅读:**这是 v4.0.0 的测试版本(beta.4),功能尚未完全稳定和加入**。v4.0.0 被设计为向前兼容,如有任何插件兼容性问题或者其他异常请在 GitHub 提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues)。在测试版本期间,您可以无缝回退到旧版本的 AstrBot,并且数据不受影响。新版本文档请[从此](https://docs-v4.astrbot.app/)访问,直到第一个 v4.0.0 稳定版本发布。
相较于 beta.3
1. 修复了主动回复时报错的问题
2. 数据迁移完毕之后引导重启程序
-15
View File
@@ -1,15 +0,0 @@
# What's Changed
> 请仔细阅读:**这是 v4.0.0 的测试版本(beta.4),功能尚未完全稳定和加入**。v4.0.0 被设计为向前兼容,如有任何插件兼容性问题或者其他异常请在 GitHub 提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues)。在测试版本期间,您可以无缝回退到旧版本的 AstrBot,并且数据不受影响。新版本文档请[从此](https://docs-v4.astrbot.app/)访问,直到第一个 v4.0.0 稳定版本发布。
相较于 beta.4
1. ‼️修复:新版本在初次保存配置之后,调用 LLM 无法获得响应,但插件指令仍可以使用的问题
2. 修复:部分情况下,Dashboard 内修改配置保存后报错 UnicodeDecodeError
3. 修复:构建 docker 镜像时同时构建 webui,并放入镜像中。
4. 修复:下载 WebUI 文件时,明确版本号,以防止 latest 不一致导致下载的 WebUI 文件版本号与实际所需不符的问题。
5. 优化:优化版本检测,考虑预发布版本,移除 `更新到最新版本` 按钮
6. 优化:增加 abconf_data 缓存,优化性能
7. 优化: 适配 qwen3 的 thinking 类模型
8. 优化: 完善对 rerank model 的可用性检测
9. 新增: 给添加 edge_tts 新增 rate, volume, pitch 参数 ([#2625](https://github.com/Soulter/AstrBot/issues/2625))
@@ -12,13 +12,7 @@
<div v-if="migrationCompleted" class="text-center py-8"> <div v-if="migrationCompleted" class="text-center py-8">
<v-icon size="64" color="success" class="mb-4">mdi-check-circle</v-icon> <v-icon size="64" color="success" class="mb-4">mdi-check-circle</v-icon>
<h3 class="mb-4">{{ t('features.migration.dialog.completed') }}</h3> <h3 class="mb-4">{{ t('features.migration.dialog.completed') }}</h3>
<p class="mb-4">{{ migrationResult?.message || t('features.migration.dialog.success') }}</p> {{ migrationResult?.message || t('features.migration.dialog.success') }}
<v-alert type="info" variant="tonal" class="mb-4">
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
{{ t('features.migration.dialog.restartRecommended') }}
</v-alert>
</div> </div>
<div v-else-if="migrating" class="migration-in-progress"> <div v-else-if="migrating" class="migration-in-progress">
@@ -86,11 +80,8 @@
<v-card-actions class="px-6 py-4"> <v-card-actions class="px-6 py-4">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<template v-if="migrationCompleted"> <template v-if="migrationCompleted">
<v-btn color="grey" variant="text" @click="handleClose"> <v-btn color="primary" variant="elevated" @click="handleClose">
{{ t('core.common.close') }} {{ t('core.common.confirm') }}
</v-btn>
<v-btn color="primary" variant="elevated" @click="restartAstrBot">
{{ t('features.migration.dialog.restartNow') }}
</v-btn> </v-btn>
</template> </template>
<template v-else> <template v-else>
@@ -105,8 +96,6 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<WaitingForRestart ref="wfr"></WaitingForRestart>
</template> </template>
<script setup> <script setup>
@@ -114,7 +103,6 @@ import { ref, computed, watch } from 'vue'
import axios from 'axios' import axios from 'axios'
import { useI18n } from '@/i18n/composables' import { useI18n } from '@/i18n/composables'
import ConsoleDisplayer from './ConsoleDisplayer.vue' import ConsoleDisplayer from './ConsoleDisplayer.vue'
import WaitingForRestart from './WaitingForRestart.vue'
const { t } = useI18n() const { t } = useI18n()
@@ -126,7 +114,6 @@ const migrationCompleted = ref(false)
const migrationResult = ref(null) const migrationResult = ref(null)
const platforms = ref([]) const platforms = ref([])
const selectedPlatforms = ref({}) const selectedPlatforms = ref({})
const wfr = ref(null)
let resolvePromise = null let resolvePromise = null
@@ -257,15 +244,6 @@ const getPlatformLabel = (platform) => {
return `${name}` return `${name}`
} }
// 重启 AstrBot
const restartAstrBot = () => {
axios.post('/api/stat/restart-core').then(() => {
if (wfr.value) {
wfr.value.check();
}
})
}
// 打开对话框的方法 // 打开对话框的方法
const open = () => { const open = () => {
isOpen.value = true isOpen.value = true
@@ -34,7 +34,7 @@
"tip": "💡 TIP:", "tip": "💡 TIP:",
"tipLink": "", "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.", "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.",
"dockerTip": "When switching versions, it 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", "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", "dockerTipLink": "watchtower",
"dockerTipContinue": "to automatically monitor and pull.", "dockerTipContinue": "to automatically monitor and pull.",
"table": { "table": {
@@ -11,8 +11,6 @@
"migratingSubtitle": "Please wait patiently, do not close this window during migration", "migratingSubtitle": "Please wait patiently, do not close this window during migration",
"migrationError": "Migration failed", "migrationError": "Migration failed",
"success": "Migration completed successfully!", "success": "Migration completed successfully!",
"completed": "Migration Completed", "completed": "Migration Completed"
"restartRecommended": "It is recommended to restart the application for all changes to take effect.",
"restartNow": "Restart Now"
} }
} }
@@ -33,7 +33,7 @@
}, },
"tip": "💡 TIP: ", "tip": "💡 TIP: ",
"tipContinue": "默认在切换版本时会下载对应版本的 WebUI 文件。WebUI 代码位于项目的 dashboard 目录,您可使用 npm 自行构建。", "tipContinue": "默认在切换版本时会下载对应版本的 WebUI 文件。WebUI 代码位于项目的 dashboard 目录,您可使用 npm 自行构建。",
"dockerTip": "切换版本时,会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用", "dockerTip": "`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用",
"dockerTipLink": "watchtower", "dockerTipLink": "watchtower",
"dockerTipContinue": "来自动监控拉取。", "dockerTipContinue": "来自动监控拉取。",
"table": { "table": {
@@ -11,8 +11,6 @@
"migratingSubtitle": "请耐心等待,迁移过程中请勿关闭此窗口", "migratingSubtitle": "请耐心等待,迁移过程中请勿关闭此窗口",
"migrationError": "迁移失败", "migrationError": "迁移失败",
"success": "迁移成功完成!", "success": "迁移成功完成!",
"completed": "迁移已完成", "completed": "迁移已完成"
"restartRecommended": "建议重启应用程序以使所有更改生效。",
"restartNow": "立即重启"
} }
} }
@@ -378,6 +378,10 @@ commonStore.getStartTime();
<!-- 发行版 --> <!-- 发行版 -->
<v-tabs-window-item key="0" v-show="tab == 0"> <v-tabs-window-item key="0" v-show="tab == 0">
<v-btn class="mt-4 mb-4" @click="switchVersion('latest')" color="primary" style="border-radius: 10px;"
:disabled="!hasNewVersion">
{{ t('core.header.updateDialog.updateToLatest') }}
</v-btn>
<div class="mb-4"> <div class="mb-4">
<small>{{ t('core.header.updateDialog.dockerTip') }} <a <small>{{ t('core.header.updateDialog.dockerTip') }} <a
href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small> href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
+1 -1
View File
@@ -481,7 +481,7 @@ export default {
if (!this.fetched) return; if (!this.fetched) return;
const postData = { const postData = {
config: JSON.parse(JSON.stringify(this.config_data)), config: this.config_data
}; };
if (this.isSystemConfig) { if (this.isSystemConfig) {
+2 -1
View File
@@ -383,7 +383,8 @@ export default {
messageType: 'success', messageType: 'success',
personaIdRules: [ personaIdRules: [
v => !!v || this.tm('validation.required'), v => !!v || this.tm('validation.required'),
v => (v && v.length >= 0) || this.tm('validation.minLength', { min: 2 }), v => (v && v.length >= 2) || this.tm('validation.minLength', { min: 2 }),
v => /^[a-zA-Z0-9_-]+$/.test(v) || this.tm('validation.alphanumeric')
], ],
systemPromptRules: [ systemPromptRules: [
v => !!v || this.tm('validation.required'), v => !!v || this.tm('validation.required'),
+3 -3
View File
@@ -44,10 +44,10 @@ async def check_dashboard_files():
if v is not None: if v is not None:
# has file # has file
if v == f"v{VERSION}": if v == f"v{VERSION}":
logger.info("WebUI 版本已是最新。") logger.info("管理面板文件已是最新。")
else: else:
logger.warning( logger.warning(
f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符" "检测到管理面板有更新。可以使用 /dashboard_update 命令更新"
) )
return return
@@ -56,7 +56,7 @@ async def check_dashboard_files():
) )
try: try:
await download_dashboard(version=f"v{VERSION}", latest=False) await download_dashboard()
except Exception as e: except Exception as e:
logger.critical(f"下载管理面板文件失败: {e}") logger.critical(f"下载管理面板文件失败: {e}")
return return
+7 -12
View File
@@ -25,18 +25,14 @@ class LongTermMemory:
def cfg(self, event: AstrMessageEvent): def cfg(self, event: AstrMessageEvent):
cfg = self.context.get_config(umo=event.unified_msg_origin) cfg = self.context.get_config(umo=event.unified_msg_origin)
try: try:
max_cnt = int(cfg["provider_ltm_settings"]["group_message_max_cnt"]) max_cnt = int(cfg["group_message_max_cnt"])
except BaseException as e: except BaseException as e:
logger.error(e) logger.error(e)
max_cnt = 300 max_cnt = 300
image_caption = ( image_caption = cfg["image_caption"]
True image_caption_prompt = cfg["image_caption_prompt"]
if cfg["provider_settings"]["default_image_caption_provider_id"] image_caption_provider_id = cfg["image_caption_provider_id"]
else False active_reply = cfg["active_reply"]
)
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
image_caption_provider_id = cfg["provider_settings"]["default_image_caption_provider_id"]
active_reply = cfg["provider_ltm_settings"]["active_reply"]
enable_active_reply = active_reply.get("enable", False) enable_active_reply = active_reply.get("enable", False)
ar_method = active_reply["method"] ar_method = active_reply["method"]
ar_possibility = active_reply["possibility_reply"] ar_possibility = active_reply["possibility_reply"]
@@ -92,9 +88,7 @@ class LongTermMemory:
if cfg["ar_whitelist"] and ( if cfg["ar_whitelist"] and (
event.unified_msg_origin not in cfg["ar_whitelist"] event.unified_msg_origin not in cfg["ar_whitelist"]
and ( and (event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"])
event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"]
)
): ):
return False return False
@@ -118,6 +112,7 @@ class LongTermMemory:
if isinstance(comp, Plain): if isinstance(comp, Plain):
final_message += f" {comp.text}" final_message += f" {comp.text}"
elif isinstance(comp, Image): elif isinstance(comp, Image):
cfg = self.cfg(event)
if cfg["image_caption"]: if cfg["image_caption"]:
try: try:
caption = await self.get_image_caption( caption = await self.get_image_caption(
+5 -5
View File
@@ -1094,7 +1094,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
@filter.command("dashboard_update") @filter.command("dashboard_update")
async def update_dashboard(self, event: AstrMessageEvent): async def update_dashboard(self, event: AstrMessageEvent):
yield event.plain_result("正在尝试更新管理面板...") yield event.plain_result("正在尝试更新管理面板...")
await download_dashboard(version=f"v{VERSION}", latest=False) await download_dashboard()
yield event.plain_result("管理面板更新完成。") yield event.plain_result("管理面板更新完成。")
@filter.command("set") @filter.command("set")
@@ -1110,9 +1110,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
@filter.command("unset") @filter.command("unset")
async def unset_variable(self, event: AstrMessageEvent, key: str): async def unset_variable(self, event: AstrMessageEvent, key: str):
uid = event.unified_msg_origin uid = event.unified_msg_origin
session_var = await sp.session_get( session_var = await sp.session_get(umo="uid", key="session_variables", default={})
umo="uid", key="session_variables", default={}
)
if key not in session_var: if key not in session_var:
yield event.plain_result("没有那个变量名。格式 /unset 变量名。") yield event.plain_result("没有那个变量名。格式 /unset 变量名。")
@@ -1178,7 +1176,9 @@ UID: {user_id} 此 ID 可用于设置管理员。
) )
return return
prompt = event.message_str prompt = self.ltm.ar_prompt
if not prompt:
prompt = event.message_str
yield event.request_llm( yield event.request_llm(
prompt=prompt, prompt=prompt,
+4 -4
View File
@@ -463,11 +463,11 @@ class Main(star.Star):
yield event.image_result(image_path) yield event.image_result(image_path)
elif match.group(1) == "FILE": elif match.group(1) == "FILE":
file_path = os.path.join(workplace_path, match.group(2)) file_path = os.path.join(workplace_path, match.group(2))
# logger.debug(f"Sending file: {file_path}") logger.debug(f"Sending file: {file_path}")
# file_s3_url = await self.file_upload(file_path) file_s3_url = await self.file_upload(file_path)
# logger.info(f"文件上传到 AstrBot 云节点: {file_s3_url}") logger.info(f"文件上传到 AstrBot 云节点: {file_s3_url}")
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
chain = [File(name=file_name, file=file_path)] chain = [File(name=file_name, file=file_s3_url)]
yield event.set_result(MessageEventResult(chain=chain)) yield event.set_result(MessageEventResult(chain=chain))
elif "Traceback (most recent call last)" in log or "[Error]: " in log: elif "Traceback (most recent call last)" in log or "[Error]: " in log:
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "AstrBot" name = "AstrBot"
version = "4.0.0-beta.5" version = "4.0.0"
description = "易上手的多平台 LLM 聊天机器人及开发框架" description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -21,7 +21,7 @@ dependencies = [
"deprecated>=1.2.18", "deprecated>=1.2.18",
"dingtalk-stream>=0.22.1", "dingtalk-stream>=0.22.1",
"docstring-parser>=0.16", "docstring-parser>=0.16",
"faiss-cpu==1.10.0", "faiss-cpu>=1.10.0",
"filelock>=3.18.0", "filelock>=3.18.0",
"google-genai>=1.14.0", "google-genai>=1.14.0",
"googlesearch-python>=1.3.0", "googlesearch-python>=1.3.0",
-3
View File
@@ -40,6 +40,3 @@ aiosqlite
py-cord>=2.6.1 py-cord>=2.6.1
slack-sdk slack-sdk
pydub pydub
sqlmodel
deprecated
sqlalchemy[asyncio]
Generated
+1575 -2249
View File
File diff suppressed because it is too large Load Diff