mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-04 11:40:13 +08:00
Compare commits
5 Commits
codex/cont
...
fix/llm_to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3828ebde18 | ||
|
|
348fe81720 | ||
|
|
44df70d2e7 | ||
|
|
952d1bfad7 | ||
|
|
0cfe4163cd |
@@ -1,4 +1,4 @@
|
||||
import logging
|
||||
|
||||
__version__ = "4.26.0-beta.12"
|
||||
__version__ = "4.26.0"
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
@@ -130,7 +130,6 @@ class LLMSummaryCompressor:
|
||||
instruction_text: str | None = None,
|
||||
compression_threshold: float = 0.82,
|
||||
token_counter: TokenCounter | None = None,
|
||||
max_recent_rounds: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize the LLM summary compressor.
|
||||
|
||||
@@ -140,18 +139,11 @@ class LLMSummaryCompressor:
|
||||
exact context. Clamped to 0-0.3.
|
||||
instruction_text: Custom instruction for summary generation.
|
||||
compression_threshold: The compression trigger threshold (default: 0.82).
|
||||
token_counter: Optional custom token counter.
|
||||
max_recent_rounds: Maximum exact recent rounds to preserve after
|
||||
summarization. If None, only the token ratio limits recent rounds.
|
||||
"""
|
||||
self.provider = provider
|
||||
self.keep_recent_ratio = min(max(float(keep_recent_ratio), 0.0), 0.3)
|
||||
self.compression_threshold = compression_threshold
|
||||
self.token_counter = token_counter or EstimateTokenCounter()
|
||||
self.max_recent_rounds = (
|
||||
None if max_recent_rounds is None else max(1, int(max_recent_rounds))
|
||||
)
|
||||
self.last_call_failed = False
|
||||
|
||||
self.instruction_text = instruction_text or (
|
||||
"Based on our full conversation history, produce a concise summary of key takeaways and/or project progress.\n"
|
||||
@@ -220,8 +212,6 @@ class LLMSummaryCompressor:
|
||||
"""
|
||||
from .round_utils import split_into_rounds
|
||||
|
||||
self.last_call_failed = False
|
||||
|
||||
rounds = split_into_rounds(messages)
|
||||
message_rounds = [
|
||||
[seg for seg in rnd if isinstance(seg, Message)] for rnd in rounds
|
||||
@@ -241,14 +231,6 @@ class LLMSummaryCompressor:
|
||||
old_rounds = old_rounds[:-1]
|
||||
recent_rounds = [latest_old_round, *recent_rounds]
|
||||
|
||||
if (
|
||||
self.max_recent_rounds is not None
|
||||
and len(recent_rounds) > self.max_recent_rounds
|
||||
):
|
||||
excess_count = len(recent_rounds) - self.max_recent_rounds
|
||||
old_rounds = old_rounds + recent_rounds[:excess_count]
|
||||
recent_rounds = recent_rounds[excess_count:]
|
||||
|
||||
if not old_rounds:
|
||||
if recent_rounds and messages and messages[-1].role == "user":
|
||||
return messages
|
||||
@@ -294,19 +276,13 @@ class LLMSummaryCompressor:
|
||||
response = await self.provider.text_chat(
|
||||
contexts=sanitized_summary_contexts,
|
||||
)
|
||||
if response.role == "err":
|
||||
logger.error(f"Failed to generate summary: {response.completion_text}")
|
||||
self.last_call_failed = True
|
||||
return messages
|
||||
summary_content = (response.completion_text or "").strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate summary: {e}")
|
||||
self.last_call_failed = True
|
||||
return messages
|
||||
|
||||
if not summary_content:
|
||||
logger.warning("LLM context compression returned an empty summary.")
|
||||
self.last_call_failed = True
|
||||
return messages
|
||||
|
||||
# Build result: system messages + summary pair + recent rounds
|
||||
|
||||
@@ -3,7 +3,6 @@ from astrbot import logger
|
||||
from ..message import Message
|
||||
from .compressor import LLMSummaryCompressor, TruncateByTurnsCompressor
|
||||
from .config import ContextConfig
|
||||
from .round_utils import count_conversation_rounds
|
||||
from .token_counter import EstimateTokenCounter
|
||||
from .truncator import ContextTruncator
|
||||
|
||||
@@ -37,11 +36,6 @@ class ContextManager:
|
||||
keep_recent_ratio=config.llm_compress_keep_recent_ratio,
|
||||
instruction_text=config.llm_compress_instruction,
|
||||
token_counter=self.token_counter,
|
||||
max_recent_rounds=(
|
||||
max(1, config.enforce_max_turns - 1)
|
||||
if config.enforce_max_turns != -1
|
||||
else None
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.compressor = TruncateByTurnsCompressor(
|
||||
@@ -62,33 +56,15 @@ class ContextManager:
|
||||
try:
|
||||
result = messages
|
||||
|
||||
# 1. 基于轮次的截断 (Enforce max turns)
|
||||
if self.config.enforce_max_turns != -1:
|
||||
turn_count = count_conversation_rounds(result)
|
||||
if turn_count > self.config.enforce_max_turns:
|
||||
should_truncate_by_turns = True
|
||||
if isinstance(self.compressor, LLMSummaryCompressor):
|
||||
logger.debug(
|
||||
"Turn limit (%s) exceeded (%s turns), "
|
||||
"trying LLM summary compression first.",
|
||||
self.config.enforce_max_turns,
|
||||
turn_count,
|
||||
)
|
||||
compressed = await self.compressor(result)
|
||||
if self.compressor.last_call_failed or compressed == result:
|
||||
logger.warning(
|
||||
"LLM summary compression failed; falling back "
|
||||
"to turn-based truncation.",
|
||||
)
|
||||
else:
|
||||
result = compressed
|
||||
should_truncate_by_turns = False
|
||||
if should_truncate_by_turns:
|
||||
result = self.truncator.truncate_by_turns(
|
||||
result,
|
||||
keep_most_recent_turns=self.config.enforce_max_turns,
|
||||
drop_turns=self.config.truncate_turns,
|
||||
)
|
||||
result = self.truncator.truncate_by_turns(
|
||||
result,
|
||||
keep_most_recent_turns=self.config.enforce_max_turns,
|
||||
drop_turns=self.config.truncate_turns,
|
||||
)
|
||||
|
||||
# 2. 基于 token 的压缩
|
||||
if self.config.max_context_tokens > 0:
|
||||
total_tokens = self.token_counter.count_tokens(
|
||||
result, trusted_token_usage
|
||||
@@ -119,17 +95,7 @@ class ContextManager:
|
||||
"""
|
||||
logger.debug("Compress triggered, starting compression...")
|
||||
|
||||
compressed = await self.compressor(messages)
|
||||
if isinstance(self.compressor, LLMSummaryCompressor):
|
||||
if self.compressor.last_call_failed:
|
||||
logger.warning(
|
||||
"LLM summary compression failed; falling back to hard "
|
||||
"truncation to keep the request within the token limit.",
|
||||
)
|
||||
else:
|
||||
messages = compressed
|
||||
else:
|
||||
messages = compressed
|
||||
messages = await self.compressor(messages)
|
||||
|
||||
# double check
|
||||
tokens_after_summary = self.token_counter.count_tokens(messages)
|
||||
@@ -147,23 +113,9 @@ class ContextManager:
|
||||
messages, tokens_after_summary, self.config.max_context_tokens
|
||||
):
|
||||
logger.info(
|
||||
"Context still exceeds max tokens after compression, applying hard truncation..."
|
||||
"Context still exceeds max tokens after compression, applying halving truncation..."
|
||||
)
|
||||
while self.compressor.should_compress(
|
||||
messages, tokens_after_summary, self.config.max_context_tokens
|
||||
):
|
||||
truncated = self.truncator.truncate_by_dropping_oldest_turns(
|
||||
messages,
|
||||
drop_turns=self.config.truncate_turns,
|
||||
)
|
||||
if truncated == messages:
|
||||
truncated = self.truncator.truncate_by_halving(messages)
|
||||
if truncated == messages:
|
||||
break
|
||||
next_tokens = self.token_counter.count_tokens(truncated)
|
||||
if next_tokens >= tokens_after_summary:
|
||||
break
|
||||
messages = truncated
|
||||
tokens_after_summary = next_tokens
|
||||
# still need compress, truncate by half
|
||||
messages = self.truncator.truncate_by_halving(messages)
|
||||
|
||||
return messages
|
||||
|
||||
@@ -35,22 +35,6 @@ def split_into_rounds(
|
||||
return rounds
|
||||
|
||||
|
||||
def count_conversation_rounds(contexts: Sequence[RoundSegment]) -> int:
|
||||
"""Count logical user conversation rounds.
|
||||
|
||||
Args:
|
||||
contexts: Flat message contexts.
|
||||
|
||||
Returns:
|
||||
Number of rounds that contain a user message.
|
||||
"""
|
||||
return sum(
|
||||
1
|
||||
for round_segments in split_into_rounds(contexts)
|
||||
if any(_segment_role(seg) == "user" for seg in round_segments)
|
||||
)
|
||||
|
||||
|
||||
def _content_to_text(content: Any) -> str:
|
||||
if isinstance(content, list):
|
||||
normalized = [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from ..message import Message
|
||||
from .round_utils import split_into_rounds
|
||||
|
||||
|
||||
class ContextTruncator:
|
||||
@@ -121,25 +120,15 @@ class ContextTruncator:
|
||||
return messages
|
||||
|
||||
system_messages, non_system_messages = self._split_system_rest(messages)
|
||||
rounds = split_into_rounds(non_system_messages)
|
||||
|
||||
round_count = sum(
|
||||
1
|
||||
for round_segments in rounds
|
||||
if any(segment.role == "user" for segment in round_segments)
|
||||
)
|
||||
if round_count <= keep_most_recent_turns:
|
||||
if len(non_system_messages) // 2 <= keep_most_recent_turns:
|
||||
return messages
|
||||
|
||||
num_to_keep = keep_most_recent_turns - drop_turns + 1
|
||||
if num_to_keep <= 0:
|
||||
truncated_contexts = []
|
||||
else:
|
||||
truncated_contexts = [
|
||||
segment
|
||||
for round_segments in rounds[-num_to_keep:]
|
||||
for segment in round_segments
|
||||
]
|
||||
truncated_contexts = non_system_messages[-num_to_keep * 2 :]
|
||||
|
||||
# Find the first user message
|
||||
index = next(
|
||||
@@ -164,21 +153,11 @@ class ContextTruncator:
|
||||
return messages
|
||||
|
||||
system_messages, non_system_messages = self._split_system_rest(messages)
|
||||
rounds = split_into_rounds(non_system_messages)
|
||||
|
||||
round_count = sum(
|
||||
1
|
||||
for round_segments in rounds
|
||||
if any(segment.role == "user" for segment in round_segments)
|
||||
)
|
||||
if round_count <= drop_turns:
|
||||
if len(non_system_messages) // 2 <= drop_turns:
|
||||
truncated_non_system = []
|
||||
else:
|
||||
truncated_non_system = [
|
||||
segment
|
||||
for round_segments in rounds[drop_turns:]
|
||||
for segment in round_segments
|
||||
]
|
||||
truncated_non_system = non_system_messages[drop_turns * 2 :]
|
||||
|
||||
# Find the first user message
|
||||
index = next(
|
||||
|
||||
@@ -136,8 +136,8 @@ DEFAULT_CONFIG = {
|
||||
),
|
||||
"llm_compress_keep_recent_ratio": 0.15,
|
||||
"llm_compress_provider_id": "",
|
||||
"max_context_length": 50,
|
||||
"dequeue_context_length": 10,
|
||||
"max_context_length": -1, # 默认不限制
|
||||
"dequeue_context_length": 1,
|
||||
"streaming_response": False,
|
||||
"show_tool_use_status": False,
|
||||
"show_tool_call_result": False,
|
||||
|
||||
@@ -249,9 +249,10 @@ class _PermissionGuardedTool(FunctionTool):
|
||||
if error is not None:
|
||||
return error
|
||||
|
||||
# Delegate to handler first (plugin tools).
|
||||
# @filter.llm_tool decorated tools have a handler attribute, which is the actual callable.
|
||||
if self._wrapped.handler is not None:
|
||||
result = self._wrapped.handler(context, **kwargs)
|
||||
event = context.context.event
|
||||
result = self._wrapped.handler(event, **kwargs)
|
||||
if _inspect.isasyncgen(result):
|
||||
last: Any = None
|
||||
async for item in result:
|
||||
@@ -261,11 +262,12 @@ class _PermissionGuardedTool(FunctionTool):
|
||||
return await result
|
||||
return result
|
||||
|
||||
# Fall back to overridden call() on subclasses (e.g. MCPTool).
|
||||
# If the tool has a "call" method that is not the default FunctionTool.call, invoke it.
|
||||
call_override = getattr(type(self._wrapped), "call", None)
|
||||
if call_override is not None and call_override is not FunctionTool.call:
|
||||
return await self._wrapped.call(context, **kwargs)
|
||||
|
||||
# Compatibility fallback: if the tool has a "run" method, invoke it. This is for legacy tools that don't use the new handler/call interface.
|
||||
run = getattr(self._wrapped, "run", None)
|
||||
if run is not None:
|
||||
event = context.context.event
|
||||
|
||||
154
changelogs/v4.26.0.md
Normal file
154
changelogs/v4.26.0.md
Normal file
@@ -0,0 +1,154 @@
|
||||
|
||||
> Note:
|
||||
> 1. WebUI “Config -> System Config” has moved to “Settings” at the bottom of the WebUI sidebar.
|
||||
> 2. It's recommended to update to v4.25.6 before updating to this version.
|
||||
>
|
||||
> 提醒:
|
||||
> 1. WebUI 的“配置 -> 系统配置”已迁移至 WebUI 侧边栏下方的“设置”。
|
||||
> 2. 建议先升级到 v4.25.6 再升级到此版本。
|
||||
>
|
||||
|
||||
- [更新日志(简体中文)](#chinese)
|
||||
- [Changelog(English)](#english)
|
||||
|
||||
<a id="chinese"></a>
|
||||
|
||||
## What's Changed
|
||||
|
||||
### ✨ 新功能
|
||||
|
||||
- 后端架构从 Quart 迁移至 FastAPI。新增多个 AstrBot OpenAPI。([#8688](https://github.com/AstrBotDevs/AstrBot/pull/8688))
|
||||
- 统一全平台消息媒体文件的处理逻辑,提升图片、音频、文件和引用消息媒体的解析一致性,对腾讯系 Silk 格式的语音文件不再使用 pilk 库。([#8764](https://github.com/AstrBotDevs/AstrBot/pull/8764))
|
||||
- WebUI 新增函数工具的逐工具权限管理,支持在工具面板中查看和切换工具权限。([#8693](https://github.com/AstrBotDevs/AstrBot/pull/8693))
|
||||
- WebUI 新增浅色、深色、跟随系统三种主题模式,并集中处理系统主题同步。([#8648](https://github.com/AstrBotDevs/AstrBot/pull/8648))
|
||||
- 重组 WebUI 系统配置页面,将系统配置入口迁移到侧边栏下方的设置区域,并优化相关设置项、自动保存和重启提示体验。([#8777](https://github.com/AstrBotDevs/AstrBot/pull/8777))
|
||||
- 新增 QQ 官方机器人 WebSocket 适配器扫码绑定流程,可通过 WebUI 一键扫码获取并回填 AppID 与 Secret,同时将 WebSocket 模板标记为推荐。([#8821](https://github.com/AstrBotDevs/AstrBot/pull/8821))
|
||||
- 增强 QQ 官方机器人群聊能力,支持群消息创建类型,并允许 Webhook 适配器在无缓存 `msg_id` 时主动发送群消息。([#8838](https://github.com/AstrBotDevs/AstrBot/pull/8838), [#8841](https://github.com/AstrBotDevs/AstrBot/pull/8841))
|
||||
- 为 OpenAI、Gemini、Anthropic 等模型请求加入可配置的重试机制,并新增请求最大重试次数配置,提升临时网络错误与 5xx 服务端错误下的稳定性。([#8893](https://github.com/AstrBotDevs/AstrBot/pull/8893))
|
||||
- 现在更新项目时,下载 AstrBot Core 会走 AstrBot 官方托管地址,提高网络稳定性。([#8888](https://github.com/AstrBotDevs/AstrBot/pull/8888))
|
||||
- 支持在请求中加载 workspace skills,并加固 workspace skill 发现流程。([#8884](https://github.com/AstrBotDevs/AstrBot/pull/8884))
|
||||
- 新增 Exa Web Search 提供商。([#8973](https://github.com/AstrBotDevs/AstrBot/pull/8973))
|
||||
- 新增 ElevenLabs TTS API Provider。([commit](https://github.com/AstrBotDevs/AstrBot/commit/0b2234936))
|
||||
- 新增启动时重置 WebUI 密码的命令行开关,便于无法登录时恢复访问。([commit](https://github.com/AstrBotDevs/AstrBot/commit/4f5075e60))
|
||||
- 新增预发布版本可见性开关。([commit](https://github.com/AstrBotDevs/AstrBot/commit/f9d408221))
|
||||
- 登录页新增公开版本详情展示。([#8986](https://github.com/AstrBotDevs/AstrBot/pull/8986))
|
||||
- 备份功能现在会包含 skills 目录。([#8700](https://github.com/AstrBotDevs/AstrBot/pull/8700))
|
||||
|
||||
### 优化
|
||||
|
||||
- 加强未来任务所有者校验,避免越权访问定时任务。([#8881](https://github.com/AstrBotDevs/AstrBot/pull/8881))
|
||||
- 优化知识库上传文件名路径穿越风险。([#8971](https://github.com/AstrBotDevs/AstrBot/pull/8971))
|
||||
- 优化插件上传文件名路径穿越风险。([#8968](https://github.com/AstrBotDevs/AstrBot/pull/8968))
|
||||
- 在受限本地文件系统工具中拒绝 hardlink 文件,避免通过工作区 hardlink 别名读写允许目录外的文件。
|
||||
- 加固沙箱文件传输与 CUA 健康检查流程,降低异常环境下的文件操作风险。([#8840](https://github.com/AstrBotDevs/AstrBot/pull/8840))
|
||||
- 消息组件日志输出现在会截断过长的 base64 字段,避免日志中出现大体积内联媒体内容。([#8591](https://github.com/AstrBotDevs/AstrBot/pull/8591))
|
||||
- 群聊上下文现在会展示被引用消息的内容。([commit](https://github.com/AstrBotDevs/AstrBot/commit/32cfcbf52))
|
||||
- 对话上下文新增当前星期信息。([#8669](https://github.com/AstrBotDevs/AstrBot/pull/8669))
|
||||
- 使用原子写入方式保存配置文件,降低写入中断导致配置损坏的概率。([#8793](https://github.com/AstrBotDevs/AstrBot/pull/8793))
|
||||
- 新增 GitHub 代理 `gh.dpik.top`,并移除失效代理 `gh.llkk.cc`。([#8772](https://github.com/AstrBotDevs/AstrBot/pull/8772), [#8761](https://github.com/AstrBotDevs/AstrBot/pull/8761))
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 aiocqhttp 平台适配器与消息事件处理器在多处 API 调用中缺少 `self_id` 路由参数的问题。([#8779](https://github.com/AstrBotDevs/AstrBot/pull/8779))
|
||||
- 修复生成平台 ID 时可能包含空白字符的问题。([#8768](https://github.com/AstrBotDevs/AstrBot/pull/8768))
|
||||
- 修复 Gemini Provider 工具定义没有正确传回模型,导致重复工具调用的问题。([#8833](https://github.com/AstrBotDevs/AstrBot/pull/8833))
|
||||
- 修复提供商源修改 ID 后保存被静默还原的问题。([#8915](https://github.com/AstrBotDevs/AstrBot/pull/8915))
|
||||
- 修复插件 LLM Tools 开关的归属校验问题,避免误操作其他插件的工具配置。([commit](https://github.com/AstrBotDevs/AstrBot/commit/fadada3d6))
|
||||
- 完善插件命名模式校验和边界场景处理。([commit](https://github.com/AstrBotDevs/AstrBot/commit/992aea986))
|
||||
- 修复插件重装后仓库来源丢失的问题。([commit](https://github.com/AstrBotDevs/AstrBot/commit/a3c25ec2c))
|
||||
- 修复子目录工具的 `handler_module_path` 不一致问题。([#8578](https://github.com/AstrBotDevs/AstrBot/pull/8578))
|
||||
- 修复 run-based tools 的保护逻辑,避免受保护工具在注册流程中被错误丢失。([#8790](https://github.com/AstrBotDevs/AstrBot/pull/8790))
|
||||
- 修复 persona 工具列表场景下系统工具被移除的问题。([#8908](https://github.com/AstrBotDevs/AstrBot/pull/8908))
|
||||
- 修复人格设定中将工具和 Skills 从指定列表切回“默认使用全部”后不生效的问题。([#8835](https://github.com/AstrBotDevs/AstrBot/pull/8835))
|
||||
- 修复新版 MCP 中 Streamable HTTP client 重命名导致的兼容问题,并保持 `mcp` 依赖小于 2。
|
||||
- 修复本地 Python 工具没有在当前 session workspace 中运行的问题。([#8792](https://github.com/AstrBotDevs/AstrBot/pull/8792))
|
||||
- 修复静态资源缺失时仍显示 WebUI ready banner 的问题。([#8804](https://github.com/AstrBotDevs/AstrBot/pull/8804))
|
||||
- 修复 Dashboard 创建文件夹时按 Enter 无法提交的问题。([#8597](https://github.com/AstrBotDevs/AstrBot/pull/8597))
|
||||
- 修复聊天输入框在非末尾位置使用输入法组合输入时可能丢失字符的问题。([#8811](https://github.com/AstrBotDevs/AstrBot/pull/8811))
|
||||
- 修复 changelog 弹窗中的锚点链接处理。([#8750](https://github.com/AstrBotDevs/AstrBot/pull/8750))
|
||||
- 修复 onboarding 平台配置与备份上传相关问题。([#8834](https://github.com/AstrBotDevs/AstrBot/pull/8834))
|
||||
- 将知识库上下文作为临时 user 内容注入,修复模型请求中知识库上下文角色不准确的问题。([#8904](https://github.com/AstrBotDevs/AstrBot/pull/8904))
|
||||
- 修复 cron 星期调度规范化问题。([#8984](https://github.com/AstrBotDevs/AstrBot/pull/8984))
|
||||
- 修复 QQ 官方平台发送消息时 At 组件被丢失的问题。([#8983](https://github.com/AstrBotDevs/AstrBot/pull/8983))
|
||||
- 修复引用消息中的 image caption 可能重复显示的问题。([#8718](https://github.com/AstrBotDevs/AstrBot/pull/8718))
|
||||
- 修复 Embedding API version 后缀被错误截断的问题。([#8736](https://github.com/AstrBotDevs/AstrBot/pull/8736))
|
||||
- 延迟导入 FAISS C 库,避免部分环境启动时进程卡住。([#8696](https://github.com/AstrBotDevs/AstrBot/pull/8696))
|
||||
- 关闭时主动释放数据库 engine,减少会话和测试环境中的资源残留。([#8650](https://github.com/AstrBotDevs/AstrBot/pull/8650))
|
||||
- 修复 CLI 版本来源不正确的问题。([#8692](https://github.com/AstrBotDevs/AstrBot/pull/8692))
|
||||
- 修复执行 `astrbot` 命令时不必要地创建 data 目录的问题。([#8932](https://github.com/AstrBotDevs/AstrBot/pull/8932))
|
||||
- 修复 sdist 构建产物路径,确保 Dashboard artifact 可被包含。([#8933](https://github.com/AstrBotDevs/AstrBot/pull/8933))
|
||||
- 修复插件页资源 token fallback。([#8970](https://github.com/AstrBotDevs/AstrBot/pull/8970))
|
||||
- 更新 `max_context_length` 与 `dequeue_context_length` 默认值。
|
||||
- 稳定 FastAPI Dashboard 路由注册测试,兼容 included router 节点。([commit](https://github.com/AstrBotDevs/AstrBot/commit/ad1b64d12))
|
||||
- 稳定 Dashboard 路由相关测试,兼容最新 FastAPI 行为。([commit](https://github.com/AstrBotDevs/AstrBot/commit/a2b6aad84))
|
||||
|
||||
<a id="english"></a>
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- Migrated the backend architecture from Quart to FastAPI and added multiple OpenAPI definitions. ([#8688](https://github.com/AstrBotDevs/AstrBot/pull/8688))
|
||||
- Unified media file handling across platforms, improving consistency for images, audio, files, and quoted-message media. Tencent Silk voice files no longer use the pilk library. ([#8764](https://github.com/AstrBotDevs/AstrBot/pull/8764))
|
||||
- Added per-tool permission management for function tools in WebUI, with support for viewing and toggling tool permissions from the tools panel. ([#8693](https://github.com/AstrBotDevs/AstrBot/pull/8693))
|
||||
- Added light, dark, and system theme modes to WebUI, with centralized system theme synchronization. ([#8648](https://github.com/AstrBotDevs/AstrBot/pull/8648))
|
||||
- Reorganized the WebUI system configuration page. The system configuration entry has moved to the Settings area at the bottom of the sidebar, with improved settings, autosave, and restart notices. ([#8777](https://github.com/AstrBotDevs/AstrBot/pull/8777))
|
||||
- Added a QR binding flow for the QQ Official Bot WebSocket adapter. WebUI can now fetch and autofill AppID and Secret through one-click QR setup, and the WebSocket template is marked as recommended. ([#8821](https://github.com/AstrBotDevs/AstrBot/pull/8821))
|
||||
- Enhanced QQ Official Bot group chat support by adding group message creation types and allowing the Webhook adapter to proactively send group messages without a cached `msg_id`. ([#8838](https://github.com/AstrBotDevs/AstrBot/pull/8838), [#8841](https://github.com/AstrBotDevs/AstrBot/pull/8841))
|
||||
- Added configurable retry handling for OpenAI, Gemini, Anthropic, and related model requests, including a maximum request retry setting for better stability during temporary network errors and 5xx server errors. ([#8893](https://github.com/AstrBotDevs/AstrBot/pull/8893))
|
||||
- AstrBot Core downloads now use AstrBot's officially hosted source during project updates, improving network stability. ([#8888](https://github.com/AstrBotDevs/AstrBot/pull/8888))
|
||||
- Added support for loading workspace skills in requests and hardened workspace skill discovery. ([#8884](https://github.com/AstrBotDevs/AstrBot/pull/8884))
|
||||
- Added Exa as a web search provider. ([#8973](https://github.com/AstrBotDevs/AstrBot/pull/8973))
|
||||
- Added the ElevenLabs TTS API provider. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/0b2234936))
|
||||
- Added a startup flag to reset the WebUI password, making it easier to recover access when login is unavailable. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/4f5075e60))
|
||||
- Added a prerelease visibility toggle. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/f9d408221))
|
||||
- Added public version details to the login page. ([#8986](https://github.com/AstrBotDevs/AstrBot/pull/8986))
|
||||
- Backups now include the skills directory. ([#8700](https://github.com/AstrBotDevs/AstrBot/pull/8700))
|
||||
|
||||
### Improvements
|
||||
|
||||
- Strengthened Future Task owner checks to prevent unauthorized scheduled-task access. ([#8881](https://github.com/AstrBotDevs/AstrBot/pull/8881))
|
||||
- Hardened knowledge base upload filename handling against path traversal risks. ([#8971](https://github.com/AstrBotDevs/AstrBot/pull/8971))
|
||||
- Hardened plugin upload filename handling against path traversal risks. ([#8968](https://github.com/AstrBotDevs/AstrBot/pull/8968))
|
||||
- Rejected hardlinked files in restricted local filesystem tools to prevent workspace hardlink aliases from reading or writing files outside allowed directories.
|
||||
- Hardened sandbox file transfers and CUA health checks to reduce file-operation risks in abnormal environments. ([#8840](https://github.com/AstrBotDevs/AstrBot/pull/8840))
|
||||
- Message component logs now truncate long base64 fields to avoid large inline media payloads in logs. ([#8591](https://github.com/AstrBotDevs/AstrBot/pull/8591))
|
||||
- Group chat context now includes quoted message content. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/32cfcbf52))
|
||||
- Added current weekday information to conversation context. ([#8669](https://github.com/AstrBotDevs/AstrBot/pull/8669))
|
||||
- Configuration files are now saved atomically, reducing the chance of corruption during interrupted writes. ([#8793](https://github.com/AstrBotDevs/AstrBot/pull/8793))
|
||||
- Added GitHub proxy `gh.dpik.top` and removed the invalid `gh.llkk.cc` proxy. ([#8772](https://github.com/AstrBotDevs/AstrBot/pull/8772), [#8761](https://github.com/AstrBotDevs/AstrBot/pull/8761))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed missing `self_id` routing parameters across multiple aiocqhttp platform adapter and message event API calls. ([#8779](https://github.com/AstrBotDevs/AstrBot/pull/8779))
|
||||
- Fixed generated platform IDs potentially containing whitespace characters. ([#8768](https://github.com/AstrBotDevs/AstrBot/pull/8768))
|
||||
- Fixed Gemini Provider tool definitions not being passed back to the model correctly, which could cause repeated tool calls. ([#8833](https://github.com/AstrBotDevs/AstrBot/pull/8833))
|
||||
- Fixed provider source ID edits being silently restored after saving. ([#8915](https://github.com/AstrBotDevs/AstrBot/pull/8915))
|
||||
- Fixed ownership checks when toggling plugin LLM tools to prevent accidental changes to other plugins' tool settings. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/fadada3d6))
|
||||
- Improved plugin naming pattern validation and edge-case handling. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/992aea986))
|
||||
- Fixed repository source information being lost after reinstalling plugins. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/a3c25ec2c))
|
||||
- Fixed inconsistent `handler_module_path` values for tools inside subdirectories. ([#8578](https://github.com/AstrBotDevs/AstrBot/pull/8578))
|
||||
- Fixed run-based tool protection so protected tools are not incorrectly dropped during registration. ([#8790](https://github.com/AstrBotDevs/AstrBot/pull/8790))
|
||||
- Fixed system tools being removed when persona tool lists are configured. ([#8908](https://github.com/AstrBotDevs/AstrBot/pull/8908))
|
||||
- Fixed persona tool and Skill settings not taking effect after switching from selected items back to "use all by default". ([#8835](https://github.com/AstrBotDevs/AstrBot/pull/8835))
|
||||
- Fixed compatibility with the renamed Streamable HTTP client in newer MCP versions while keeping the `mcp` dependency below 2.
|
||||
- Fixed local Python tools not running inside the current session workspace. ([#8792](https://github.com/AstrBotDevs/AstrBot/pull/8792))
|
||||
- Fixed the WebUI ready banner being shown even when static assets are missing. ([#8804](https://github.com/AstrBotDevs/AstrBot/pull/8804))
|
||||
- Fixed Dashboard folder creation not being submitted when pressing Enter. ([#8597](https://github.com/AstrBotDevs/AstrBot/pull/8597))
|
||||
- Fixed possible IME composition character loss when typing at a non-terminal cursor position in the chat input. ([#8811](https://github.com/AstrBotDevs/AstrBot/pull/8811))
|
||||
- Fixed changelog anchor link handling in the Dashboard dialog. ([#8750](https://github.com/AstrBotDevs/AstrBot/pull/8750))
|
||||
- Fixed onboarding platform configuration and backup upload issues. ([#8834](https://github.com/AstrBotDevs/AstrBot/pull/8834))
|
||||
- Injected knowledge base context as temporary user content, fixing the role used for knowledge context in model requests. ([#8904](https://github.com/AstrBotDevs/AstrBot/pull/8904))
|
||||
- Fixed cron weekday scheduling normalization. ([#8984](https://github.com/AstrBotDevs/AstrBot/pull/8984))
|
||||
- Fixed At components being dropped when sending messages on the QQ Official platform. ([#8983](https://github.com/AstrBotDevs/AstrBot/pull/8983))
|
||||
- Fixed duplicate captions for quoted images. ([#8718](https://github.com/AstrBotDevs/AstrBot/pull/8718))
|
||||
- Fixed Embedding API version suffixes being truncated incorrectly. ([#8736](https://github.com/AstrBotDevs/AstrBot/pull/8736))
|
||||
- Deferred FAISS C library imports to avoid startup hangs in some environments. ([#8696](https://github.com/AstrBotDevs/AstrBot/pull/8696))
|
||||
- Disposed the database engine on shutdown to reduce resource leftovers in sessions and tests. ([#8650](https://github.com/AstrBotDevs/AstrBot/pull/8650))
|
||||
- Fixed the CLI version source. ([#8692](https://github.com/AstrBotDevs/AstrBot/pull/8692))
|
||||
- Fixed unnecessary data directory creation when executing the `astrbot` command. ([#8932](https://github.com/AstrBotDevs/AstrBot/pull/8932))
|
||||
- Fixed the sdist build artifact path so the Dashboard artifact can be included. ([#8933](https://github.com/AstrBotDevs/AstrBot/pull/8933))
|
||||
- Fixed plugin page asset token fallback. ([#8970](https://github.com/AstrBotDevs/AstrBot/pull/8970))
|
||||
- Updated the `max_context_length` and `dequeue_context_length` defaults.
|
||||
- Stabilized FastAPI Dashboard route registration tests for included router nodes. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/ad1b64d12))
|
||||
- Stabilized Dashboard route tests for the latest FastAPI behavior. ([commit](https://github.com/AstrBotDevs/AstrBot/commit/a2b6aad84))
|
||||
5
docs/wrangler.toml
Normal file
5
docs/wrangler.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
name = "astrbot-docs"
|
||||
compatibility_date = "2026-06-25"
|
||||
|
||||
[assets]
|
||||
directory = "./.vitepress/dist"
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.26.0-beta.12"
|
||||
version = "4.26.0"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
|
||||
@@ -12,7 +12,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from astrbot.core.agent.context.config import ContextConfig
|
||||
from astrbot.core.agent.context.manager import ContextManager
|
||||
from astrbot.core.agent.context.round_utils import count_conversation_rounds
|
||||
from astrbot.core.agent.message import AudioURLPart, ImageURLPart, Message, TextPart
|
||||
from astrbot.core.provider.entities import LLMResponse
|
||||
|
||||
@@ -43,26 +42,6 @@ class MockProvider:
|
||||
return MagicMock(id="test_provider", type="openai")
|
||||
|
||||
|
||||
class MessageCountTokenCounter:
|
||||
"""Token counter that assigns a fixed cost to each message."""
|
||||
|
||||
def count_tokens(
|
||||
self, messages: list[Message], trusted_token_usage: int = 0
|
||||
) -> int:
|
||||
"""Count tokens by message count for deterministic tests.
|
||||
|
||||
Args:
|
||||
messages: The messages to count.
|
||||
trusted_token_usage: A trusted token count to return when present.
|
||||
|
||||
Returns:
|
||||
The deterministic token count.
|
||||
"""
|
||||
if trusted_token_usage > 0:
|
||||
return trusted_token_usage
|
||||
return len(messages) * 100
|
||||
|
||||
|
||||
class TestContextManager:
|
||||
"""Test suite for ContextManager."""
|
||||
|
||||
@@ -488,74 +467,6 @@ class TestContextManager:
|
||||
assert len(system_msgs) >= 1
|
||||
assert system_msgs[0].content == "System instruction"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_enforce_max_turns_uses_summary_first(self):
|
||||
"""LLM strategy should summarize before falling back to turn truncation."""
|
||||
provider = MockProvider()
|
||||
config = ContextConfig(
|
||||
enforce_max_turns=1,
|
||||
llm_compress_provider=provider, # type: ignore[arg-type]
|
||||
llm_compress_keep_recent_ratio=0,
|
||||
)
|
||||
manager = ContextManager(config)
|
||||
messages = [
|
||||
self.create_message("user", "First"),
|
||||
self.create_message("assistant", "First answer"),
|
||||
self.create_message("user", "Second"),
|
||||
self.create_message("assistant", "Second answer"),
|
||||
self.create_message("user", "Continue"),
|
||||
]
|
||||
|
||||
result = await manager.process(messages)
|
||||
|
||||
assert provider.last_text_chat_kwargs is not None
|
||||
assert len(result) < len(messages)
|
||||
assert result[-1] is messages[-1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_enforce_max_turns_caps_recent_rounds(self):
|
||||
"""LLM summary should cap exact recent rounds for max-turn enforcement."""
|
||||
provider = MockProvider()
|
||||
config = ContextConfig(
|
||||
enforce_max_turns=2,
|
||||
llm_compress_provider=provider, # type: ignore[arg-type]
|
||||
llm_compress_keep_recent_ratio=0.3,
|
||||
custom_token_counter=MessageCountTokenCounter(),
|
||||
)
|
||||
manager = ContextManager(config)
|
||||
messages = self.create_messages(20)
|
||||
|
||||
result = await manager.process(messages)
|
||||
|
||||
assert provider.last_text_chat_kwargs is not None
|
||||
assert count_conversation_rounds(result) <= config.enforce_max_turns
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enforce_max_turns_counts_tool_chain_as_one_round(self):
|
||||
"""Tool messages in one round should not inflate turn count."""
|
||||
config = ContextConfig(enforce_max_turns=1, truncate_turns=1)
|
||||
manager = ContextManager(config)
|
||||
messages = [
|
||||
self.create_message("user", "Run a tool"),
|
||||
Message(
|
||||
role="assistant",
|
||||
content="Calling tool",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
),
|
||||
Message(role="tool", content="Tool result", tool_call_id="call_1"),
|
||||
self.create_message("assistant", "Done"),
|
||||
]
|
||||
|
||||
result = await manager.process(messages)
|
||||
|
||||
assert result == messages
|
||||
|
||||
# ==================== Token-based Compression Tests ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1055,27 +966,6 @@ class TestContextManager:
|
||||
# Should have been compressed
|
||||
assert len(result) <= len(messages)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_failure_falls_back_until_token_threshold(self):
|
||||
"""Failed LLM compression should hard truncate until tokens are acceptable."""
|
||||
mock_provider = MockProvider()
|
||||
mock_provider.text_chat = AsyncMock(
|
||||
return_value=LLMResponse(role="err", completion_text="compress failed")
|
||||
)
|
||||
config = ContextConfig(
|
||||
max_context_tokens=300,
|
||||
truncate_turns=1,
|
||||
llm_compress_provider=mock_provider, # type: ignore[arg-type]
|
||||
custom_token_counter=MessageCountTokenCounter(),
|
||||
)
|
||||
manager = ContextManager(config)
|
||||
messages = self.create_messages(10)
|
||||
|
||||
result = await manager.process(messages)
|
||||
|
||||
assert len(result) == 2
|
||||
assert manager.token_counter.count_tokens(result) <= 246
|
||||
|
||||
# ==================== split_into_rounds Tests ====================
|
||||
|
||||
def test_split_rounds_ensures_user_start(self):
|
||||
|
||||
@@ -134,32 +134,6 @@ class TestContextTruncator:
|
||||
assert len(result) == 6
|
||||
assert result == messages
|
||||
|
||||
def test_truncate_by_turns_counts_tool_chain_as_one_round(self):
|
||||
"""Tool calls/results inside one round should not count as extra turns."""
|
||||
truncator = ContextTruncator()
|
||||
messages = [
|
||||
self.create_message("user", "Run a tool"),
|
||||
Message(
|
||||
role="assistant",
|
||||
content="Calling tool",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
),
|
||||
Message(role="tool", content="Tool result", tool_call_id="call_1"),
|
||||
self.create_message("assistant", "Done"),
|
||||
]
|
||||
|
||||
result = truncator.truncate_by_turns(
|
||||
messages, keep_most_recent_turns=1, drop_turns=1
|
||||
)
|
||||
|
||||
assert result == messages
|
||||
|
||||
def test_truncate_by_turns_ensures_user_first(self):
|
||||
"""Test that truncate_by_turns ensures user message comes first."""
|
||||
truncator = ContextTruncator()
|
||||
|
||||
@@ -174,16 +174,19 @@ async def test_check_permission_passes_for_member_when_configured_member():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_guarded_tool_delegates_when_permission_passes():
|
||||
async def test_guarded_tool_delegates_handler_with_event_when_permission_passes():
|
||||
_clear_tool_permissions()
|
||||
mgr = FunctionToolManager()
|
||||
|
||||
called = False
|
||||
received_event = None
|
||||
|
||||
async def handler(ctx, **kw):
|
||||
async def handler(event, **kw):
|
||||
nonlocal called
|
||||
nonlocal received_event
|
||||
called = True
|
||||
return "ok"
|
||||
received_event = event
|
||||
return f"ok:{event.get_sender_id()}:{kw['value']}"
|
||||
|
||||
wrapped = FunctionTool(
|
||||
name="delegated",
|
||||
@@ -194,9 +197,10 @@ async def test_guarded_tool_delegates_when_permission_passes():
|
||||
guarded = _PermissionGuardedTool(wrapped, mgr)
|
||||
context = _make_context(role="member")
|
||||
|
||||
result = await guarded.call(context)
|
||||
result = await guarded.call(context, value="sentinel")
|
||||
assert called
|
||||
assert result == "ok"
|
||||
assert received_event is context.context.event
|
||||
assert result == "ok:user_123:sentinel"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -280,7 +284,8 @@ async def test_guarded_tool_handles_async_generator_handler():
|
||||
_clear_tool_permissions()
|
||||
mgr = FunctionToolManager()
|
||||
|
||||
async def gen_handler(ctx, **kw): # type: ignore[misc]
|
||||
async def gen_handler(event, **kw): # type: ignore[misc]
|
||||
assert event is context.context.event
|
||||
yield "A"
|
||||
yield "B"
|
||||
yield "C"
|
||||
|
||||
Reference in New Issue
Block a user