Compare commits

..

16 Commits

Author SHA1 Message Date
Soulter
889ce97f2b fix: enhance content validation in Message model to allow None for assistant role with tool_calls 2025-11-30 23:40:24 +08:00
Soulter
9588d87d11 fix: assistant message validation error when tool_call exists but content not exists 2025-11-30 23:34:53 +08:00
Dt8333
a130db5cf4 fix: 将 Graceful shutdown 的异常改为 KeyboardInterrupt (#3855) 2025-11-30 20:31:17 +08:00
邹永赫
7faeb5cea8 Merge pull request #3850 from zouyonghe/feature/plugin-upgrade-all
增加升级所有插件按钮
2025-11-30 15:12:36 +09:00
ZouYonghe
8d3ff61e0d Format plugin route with ruff 2025-11-30 11:56:24 +08:00
ZouYonghe
4c03e82570 Fix plugin update JSON parsing and concurrency handling 2025-11-30 11:50:46 +08:00
ZouYonghe
e7e8664ab4 chore: tweak update all label 2025-11-30 11:18:30 +08:00
ZouYonghe
1dd1623e7d feat: batch update plugins via new api 2025-11-30 11:11:36 +08:00
ZouYonghe
80d8161d58 feat: add update all plugins action 2025-11-30 10:40:46 +08:00
Soulter
fc80d7d681 chore: bump version to 4.7.3 2025-11-30 00:42:49 +08:00
Soulter
c2f036b27c chore: bump vertion to 4.7.2 2025-11-30 00:33:07 +08:00
Soulter
4087bbb512 perf: set content attribute optional to AssistantMessageSegment for enhanced message handling
fixes: #3843
2025-11-30 00:32:00 +08:00
Soulter
e1c728582d chore: bump version to 4.7.2 2025-11-30 00:18:23 +08:00
Oscar Shaw
93c69a639a feat: 新增群聊模式下的专用图片转述模型配置 (#3822)
* feat: add image caption provider configuration for group chat

- Introduced `image_caption_provider_id` to allow separate configuration for group chat image understanding.
- Updated metadata and hints in English and Chinese for clarity on new settings.
- Adjusted logic in long term memory to utilize the new provider ID for image captioning.

* fix: format

* Fix logic for image caption and active reply settings

* Fix indentation and formatting in long_term_memory.py

* chore: ruff format

---------

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>
2025-11-29 23:53:32 +08:00
Soulter
a7fdc98b29 fix: third party agent runner cannot run properly when using non-default config file
fix: #3815
2025-11-29 23:45:12 +08:00
Soulter
85b7f104df fix: remove unnecessary provider check (#3846)
fixes: #3815
2025-11-29 23:15:19 +08:00
14 changed files with 218 additions and 32 deletions

View File

@@ -1 +1 @@
__version__ = "4.7.1"
__version__ = "4.7.3"

View File

@@ -3,7 +3,7 @@
from typing import Any, ClassVar, Literal, cast
from pydantic import BaseModel, GetCoreSchemaHandler
from pydantic import BaseModel, GetCoreSchemaHandler, model_validator
from pydantic_core import core_schema
@@ -145,22 +145,39 @@ class Message(BaseModel):
"tool",
]
content: str | list[ContentPart]
content: str | list[ContentPart] | None = None
"""The content of the message."""
tool_calls: list[ToolCall] | list[dict] | None = None
"""The tool calls of the message."""
tool_call_id: str | None = None
"""The ID of the tool call."""
@model_validator(mode="after")
def check_content_required(self):
# assistant + tool_calls is not None: allow content to be None
if self.role == "assistant" and self.tool_calls is not None:
return self
# other all cases: content is required
if self.content is None:
raise ValueError(
"content is required unless role='assistant' and tool_calls is not None"
)
return self
class AssistantMessageSegment(Message):
"""A message segment from the assistant."""
role: Literal["assistant"] = "assistant"
tool_calls: list[ToolCall] | list[dict] | None = None
class ToolCallMessageSegment(Message):
"""A message segment representing a tool call."""
role: Literal["tool"] = "tool"
tool_call_id: str
class UserMessageSegment(Message):

View File

@@ -4,7 +4,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.7.1"
VERSION = "4.7.3"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
# 默认配置
@@ -90,6 +90,7 @@ DEFAULT_CONFIG = {
"group_icl_enable": False,
"group_message_max_cnt": 300,
"image_caption": False,
"image_caption_provider_id": "",
"active_reply": {
"enable": False,
"method": "possibility_reply",
@@ -2109,6 +2110,9 @@ CONFIG_METADATA_2 = {
"image_caption": {
"type": "bool",
},
"image_caption_provider_id": {
"type": "string",
},
"image_caption_prompt": {
"type": "string",
},
@@ -2785,7 +2789,16 @@ CONFIG_METADATA_3 = {
"provider_ltm_settings.image_caption": {
"description": "自动理解图片",
"type": "bool",
"hint": "需要设置默认图片转述模型。",
"hint": "需要设置群聊图片转述模型。",
},
"provider_ltm_settings.image_caption_provider_id": {
"description": "群聊图片转述模型",
"type": "string",
"_special": "select_provider",
"hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。",
"condition": {
"provider_ltm_settings.image_caption": True,
},
},
"provider_ltm_settings.active_reply.enable": {
"description": "主动回复",

View File

@@ -2,7 +2,7 @@ import asyncio
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING
from astrbot.core import logger
from astrbot.core import astrbot_config, logger
from astrbot.core.agent.runners.coze.coze_agent_runner import CozeAgentRunner
from astrbot.core.agent.runners.dashscope.dashscope_agent_runner import (
DashscopeAgentRunner,
@@ -88,7 +88,7 @@ class ThirdPartyAgentSubStage(Stage):
return
self.prov_cfg: dict = next(
(p for p in self.conf["provider"] if p["id"] == self.prov_id),
(p for p in astrbot_config["provider"] if p["id"] == self.prov_id),
{},
)
if not self.prov_id:

View File

@@ -250,7 +250,7 @@ class DingtalkPlatformAdapter(Platform):
async def terminate(self):
def monkey_patch_close():
raise Exception("Graceful shutdown")
raise KeyboardInterrupt("Graceful shutdown")
self.client_.open_connection = monkey_patch_close
await self.client_.websocket.close(code=1000, reason="Graceful shutdown")

View File

@@ -1,3 +1,4 @@
import asyncio
import json
import os
import ssl
@@ -19,6 +20,10 @@ from astrbot.core.star.star_manager import PluginManager
from .route import Response, Route, RouteContext
PLUGIN_UPDATE_CONCURRENCY = (
3 # limit concurrent updates to avoid overwhelming plugin sources
)
class PluginRoute(Route):
def __init__(
@@ -33,6 +38,7 @@ class PluginRoute(Route):
"/plugin/install": ("POST", self.install_plugin),
"/plugin/install-upload": ("POST", self.install_plugin_upload),
"/plugin/update": ("POST", self.update_plugin),
"/plugin/update-all": ("POST", self.update_all_plugins),
"/plugin/uninstall": ("POST", self.uninstall_plugin),
"/plugin/market_list": ("GET", self.get_online_plugins),
"/plugin/off": ("POST", self.off_plugin),
@@ -63,7 +69,7 @@ class PluginRoute(Route):
.__dict__
)
data = await request.json
data = await request.get_json()
plugin_name = data.get("name", None)
try:
success, message = await self.plugin_manager.reload(plugin_name)
@@ -346,7 +352,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
repo_url = post_data["url"]
proxy: str = post_data.get("proxy", None)
@@ -393,7 +399,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
delete_config = post_data.get("delete_config", False)
delete_data = post_data.get("delete_data", False)
@@ -418,7 +424,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
proxy: str = post_data.get("proxy", None)
try:
@@ -432,6 +438,59 @@ class PluginRoute(Route):
logger.error(f"/api/plugin/update: {traceback.format_exc()}")
return Response().error(str(e)).__dict__
async def update_all_plugins(self):
if DEMO_MODE:
return (
Response()
.error("You are not permitted to do this operation in demo mode")
.__dict__
)
post_data = await request.get_json()
plugin_names: list[str] = post_data.get("names") or []
proxy: str = post_data.get("proxy", "")
if not isinstance(plugin_names, list) or not plugin_names:
return Response().error("插件列表不能为空").__dict__
results = []
sem = asyncio.Semaphore(PLUGIN_UPDATE_CONCURRENCY)
async def _update_one(name: str):
async with sem:
try:
logger.info(f"批量更新插件 {name}")
await self.plugin_manager.update_plugin(name, proxy)
return {"name": name, "status": "ok", "message": "更新成功"}
except Exception as e:
logger.error(
f"/api/plugin/update-all: 更新插件 {name} 失败: {traceback.format_exc()}",
)
return {"name": name, "status": "error", "message": str(e)}
raw_results = await asyncio.gather(
*(_update_one(name) for name in plugin_names),
return_exceptions=True,
)
for name, result in zip(plugin_names, raw_results):
if isinstance(result, asyncio.CancelledError):
raise result
if isinstance(result, BaseException):
results.append(
{"name": name, "status": "error", "message": str(result)}
)
else:
results.append(result)
failed = [r for r in results if r["status"] == "error"]
message = (
"批量更新完成,全部成功。"
if not failed
else f"批量更新完成,其中 {len(failed)}/{len(results)} 个插件失败。"
)
return Response().ok({"results": results}, message).__dict__
async def off_plugin(self):
if DEMO_MODE:
return (
@@ -440,7 +499,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
try:
await self.plugin_manager.turn_off_plugin(plugin_name)
@@ -458,7 +517,7 @@ class PluginRoute(Route):
.__dict__
)
post_data = await request.json
post_data = await request.get_json()
plugin_name = post_data["name"]
try:
await self.plugin_manager.turn_on_plugin(plugin_name)

25
changelogs/v4.7.3.md Normal file
View File

@@ -0,0 +1,25 @@
## What's Changed
1. 修复使用非默认配置文件情况下时,第三方 Agent Runner (Dify、Coze、阿里云百炼应用等)无法正常工作的问题
2. 修复当“聊天模型”未设置,并且模型提供商中仅有 Agent Runner 时,无法正常使用 Agent Runner 的问题
3. 修复部分情况下报错 `pydantic_core._pydantic_core.ValidationError: 1 validation error for Message content` 的问题
4. 新增群聊模式下的专用图片转述模型配置 ([#3822](https://github.com/AstrBotDevs/AstrBot/issues/3822))
---
重构:
- 将 Dify、Coze、阿里云百炼应用等 LLMOps 提供商迁移到 Agent 执行器层,理清和本地 Agent 执行器的边界。详见:[Agent 执行器](https://docs.astrbot.app/use/agent-runner.html)
- 将「会话管理」功能重构为「自定义规则」功能,理清和多配置文件功能的边界。详见:[自定义规则](https://docs.astrbot.app/use/custom-rules.html)
优化:
- Dify、阿里云百炼应用支持流式输出
- 防止分段回复正则表达式解析错误导致消息不发送
- 群聊上下文感知记录 At 信息
- 优化模型提供商页面的测试提供商功能
新增:
- 支持在配置文件页面快速测试对话
- 为配置文件配置项内容添加国际化支持
修复:
- 在更新 MCP Server 配置后MCP 无法正常重启的问题

View File

@@ -379,7 +379,11 @@
},
"image_caption": {
"description": "Auto-understand Images",
"hint": "Requires setting a default image caption model."
"hint": "Requires setting a group chat image caption model."
},
"image_caption_provider_id": {
"description": "Group Chat Image Caption Model",
"hint": "Used for image understanding in group chat context awareness, configured separately from the default image caption model."
},
"active_reply": {
"enable": {

View File

@@ -32,7 +32,8 @@
"actions": "Actions",
"back": "Back",
"selectFile": "Select File",
"refresh": "Refresh"
"refresh": "Refresh",
"updateAll": "Update All"
},
"status": {
"enabled": "Enabled",
@@ -141,7 +142,9 @@
"confirmDelete": "Are you sure you want to delete this extension?",
"fillUrlOrFile": "Please fill in extension URL or upload extension file",
"dontFillBoth": "Please don't fill in both extension URL and upload file",
"supportedFormats": "Supports .zip extension files"
"supportedFormats": "Supports .zip extension files",
"updateAllSuccess": "All upgradable extensions have been updated!",
"updateAllFailed": "{failed} of {total} extensions failed to update:"
},
"upload": {
"fromFile": "Install from File",

View File

@@ -379,7 +379,11 @@
},
"image_caption": {
"description": "自动理解图片",
"hint": "需要设置默认图片转述模型。"
"hint": "需要设置群聊图片转述模型。"
},
"image_caption_provider_id": {
"description": "群聊图片转述模型",
"hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。"
},
"active_reply": {
"enable": {

View File

@@ -32,7 +32,8 @@
"actions": "操作",
"back": "返回",
"selectFile": "选择文件",
"refresh": "刷新"
"refresh": "刷新",
"updateAll": "更新全部插件"
},
"status": {
"enabled": "启用",
@@ -141,7 +142,9 @@
"confirmDelete": "确定要删除插件吗?",
"fillUrlOrFile": "请填写插件链接或上传插件文件",
"dontFillBoth": "请不要同时填写插件链接和上传文件",
"supportedFormats": "支持 .zip 格式的插件文件"
"supportedFormats": "支持 .zip 格式的插件文件",
"updateAllSuccess": "所有可更新的插件都已更新!",
"updateAllFailed": "有 {failed}/{total} 个插件更新失败:"
},
"upload": {
"fromFile": "从文件安装",

View File

@@ -42,6 +42,7 @@ const loadingDialog = reactive({
const showPluginInfoDialog = ref(false);
const selectedPlugin = ref({});
const curr_namespace = ref("");
const updatingAll = ref(false);
const readmeDialog = reactive({
show: false,
@@ -226,6 +227,10 @@ const paginatedPlugins = computed(() => {
return sortedPlugins.value.slice(start, end);
});
const updatableExtensions = computed(() => {
return extension_data?.data?.filter(ext => ext.has_update) || [];
});
// 方法
const toggleShowReserved = () => {
showReserved.value = !showReserved.value;
@@ -372,6 +377,56 @@ const updateExtension = async (extension_name) => {
}
};
const updateAllExtensions = async () => {
if (updatingAll.value || updatableExtensions.value.length === 0) return;
updatingAll.value = true;
loadingDialog.title = tm('status.loading');
loadingDialog.statusCode = 0;
loadingDialog.result = "";
loadingDialog.show = true;
const targets = updatableExtensions.value.map(ext => ext.name);
try {
const res = await axios.post('/api/plugin/update-all', {
names: targets,
proxy: localStorage.getItem('selectedGitHubProxy') || ""
});
if (res.data.status === "error") {
onLoadingDialogResult(2, res.data.message || tm('messages.updateAllFailed', {
failed: targets.length,
total: targets.length
}), -1);
return;
}
const results = res.data.data?.results || [];
const failures = results.filter(r => r.status !== 'ok');
try {
await getExtensions();
} catch (err) {
const errorMsg = err.response?.data?.message || err.message || String(err);
failures.push({ name: 'refresh', status: 'error', message: errorMsg });
}
if (failures.length === 0) {
onLoadingDialogResult(1, tm('messages.updateAllSuccess'));
} else {
const failureText = tm('messages.updateAllFailed', {
failed: failures.length,
total: targets.length
});
const detail = failures.map(f => `${f.name}: ${f.message}`).join('\n');
onLoadingDialogResult(2, `${failureText}\n${detail}`, -1);
}
} catch (err) {
const errorMsg = err.response?.data?.message || err.message || String(err);
onLoadingDialogResult(2, errorMsg, -1);
} finally {
updatingAll.value = false;
}
};
const pluginOn = async (extension) => {
try {
const res = await axios.post('/api/plugin/on', { name: extension.name });
@@ -720,6 +775,12 @@ watch(marketSearch, (newVal) => {
{{ showReserved ? tm('buttons.hideSystemPlugins') : tm('buttons.showSystemPlugins') }}
</v-btn>
<v-btn class="ml-2" color="warning" variant="tonal" :disabled="updatableExtensions.length === 0"
:loading="updatingAll" @click="updateAllExtensions">
<v-icon>mdi-update</v-icon>
{{ tm('buttons.updateAll') }}
</v-btn>
<v-btn class="ml-2" color="primary" variant="tonal" @click="dialog = true">
<v-icon>mdi-plus</v-icon>
{{ tm('buttons.install') }}

View File

@@ -30,16 +30,13 @@ class LongTermMemory:
except BaseException as e:
logger.error(e)
max_cnt = 300
image_caption = (
True
if cfg["provider_settings"]["default_image_caption_provider_id"]
and cfg["provider_ltm_settings"]["image_caption"]
else False
)
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
image_caption_provider_id = cfg["provider_settings"][
"default_image_caption_provider_id"
]
image_caption_provider_id = cfg["provider_ltm_settings"].get(
"image_caption_provider_id"
)
image_caption = cfg["provider_ltm_settings"]["image_caption"] and bool(
image_caption_provider_id
)
active_reply = cfg["provider_ltm_settings"]["active_reply"]
enable_active_reply = active_reply.get("enable", False)
ar_method = active_reply["method"]

View File

@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.7.1"
version = "4.7.3"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
requires-python = ">=3.10"