mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-09 14:30:12 +08:00
Compare commits
3 Commits
feat/conv-
...
v4.23.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ab45fcb5 | ||
|
|
1efe4fd60e | ||
|
|
c5ab4f7263 |
@@ -1,3 +1,6 @@
|
||||
from sqlalchemy import case, func, select
|
||||
from sqlmodel import col
|
||||
|
||||
from astrbot.api import sp, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.core import logger
|
||||
@@ -7,6 +10,7 @@ from astrbot.core.agent.runners.deerflow.constants import (
|
||||
DEERFLOW_THREAD_ID_KEY,
|
||||
)
|
||||
from astrbot.core.agent.runners.deerflow.deerflow_api_client import DeerFlowAPIClient
|
||||
from astrbot.core.db.po import ProviderStat
|
||||
from astrbot.core.utils.active_event_registry import active_event_registry
|
||||
|
||||
from .utils.rst_scene import RstScene
|
||||
@@ -246,3 +250,62 @@ class ConversationCommands:
|
||||
f"✅ Switched to new conversation: {cid[:4]}。"
|
||||
),
|
||||
)
|
||||
|
||||
async def stats(self, message: AstrMessageEvent) -> None:
|
||||
"""Show token usage statistics for the current conversation."""
|
||||
umo = message.unified_msg_origin
|
||||
cid = await self.context.conversation_manager.get_curr_conversation_id(umo)
|
||||
|
||||
if not cid:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"❌ You are not in a conversation. Use /new to create one."
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
db = self.context.get_db()
|
||||
async with db.get_db() as session:
|
||||
result = await session.execute(
|
||||
select(
|
||||
func.count(case((col(ProviderStat.id).is_not(None), 1))).label(
|
||||
"record_count",
|
||||
),
|
||||
func.coalesce(func.sum(ProviderStat.token_input_other), 0).label(
|
||||
"total_input_other",
|
||||
),
|
||||
func.coalesce(func.sum(ProviderStat.token_input_cached), 0).label(
|
||||
"total_input_cached",
|
||||
),
|
||||
func.coalesce(func.sum(ProviderStat.token_output), 0).label(
|
||||
"total_output",
|
||||
),
|
||||
).where(
|
||||
col(ProviderStat.agent_type) == "internal",
|
||||
col(ProviderStat.conversation_id) == cid,
|
||||
)
|
||||
)
|
||||
stats = result.one()
|
||||
|
||||
if stats.record_count == 0:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"📊 No stats available for this conversation yet."
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
total_input_other = stats.total_input_other
|
||||
total_input_cached = stats.total_input_cached
|
||||
total_output = stats.total_output
|
||||
total_tokens = total_input_other + total_input_cached + total_output
|
||||
|
||||
ret = (
|
||||
f"📊 Conversation Token usage (ID: {cid[:8]}...)\n"
|
||||
f"Total: {total_tokens:,}\n"
|
||||
f"Input (cached): {total_input_cached:,}\n"
|
||||
f"Input (other): {total_input_other:,}\n"
|
||||
f"Output: {total_output:,}\n"
|
||||
)
|
||||
|
||||
message.set_result(MessageEventResult().message(ret))
|
||||
|
||||
@@ -47,6 +47,11 @@ class Main(star.Star):
|
||||
"""Create new conversation"""
|
||||
await self.conversation_c.new_conv(message)
|
||||
|
||||
@filter.command("stats")
|
||||
async def stats(self, message: AstrMessageEvent) -> None:
|
||||
"""Show token usage statistics for the current conversation"""
|
||||
await self.conversation_c.stats(message)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("provider")
|
||||
async def provider(
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.23.5"
|
||||
__version__ = "4.23.6"
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.23.5"
|
||||
VERSION = "4.23.6"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
PERSONAL_WECHAT_CONFIG_METADATA = {
|
||||
"weixin_oc_base_url": {
|
||||
|
||||
@@ -243,6 +243,7 @@ class StatRoute(Route):
|
||||
total_by_umo: dict[str, int] = defaultdict(int)
|
||||
total_by_bucket: dict[int, int] = defaultdict(int)
|
||||
range_total_tokens = 0
|
||||
range_total_output_tokens = 0
|
||||
range_total_calls = 0
|
||||
range_success_calls = 0
|
||||
range_ttft_total_ms = 0.0
|
||||
@@ -286,6 +287,7 @@ class StatRoute(Route):
|
||||
record.end_time - record.start_time
|
||||
) * 1000
|
||||
range_duration_samples += 1
|
||||
range_total_output_tokens += record.token_output
|
||||
|
||||
if created_at_local >= today_start_local:
|
||||
today_total_calls += 1
|
||||
@@ -371,7 +373,8 @@ class StatRoute(Route):
|
||||
else 0
|
||||
),
|
||||
"range_avg_tpm": (
|
||||
range_total_tokens / (range_duration_total_ms / 1000 / 60)
|
||||
range_total_output_tokens
|
||||
/ (range_duration_total_ms / 1000 / 60)
|
||||
if range_duration_total_ms > 0
|
||||
else 0
|
||||
),
|
||||
|
||||
56
changelogs/v4.23.6.md
Normal file
56
changelogs/v4.23.6.md
Normal file
@@ -0,0 +1,56 @@
|
||||
- [更新日志(简体中文)](#chinese)
|
||||
- [Changelog(English)](#english)
|
||||
|
||||
<a id="chinese"></a>
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增 `/stats` 命令,可查看当前会话的 Token 使用统计,并按总量、输入(缓存)、输入(其他)与输出拆分展示。([#7831](https://github.com/AstrBotDevs/AstrBot/pull/7831))
|
||||
- 新增 Firecrawl Web 搜索与网页提取工具,支持搜索结果处理、网页内容提取、会话管理、请求校验与相关测试。([#7764](https://github.com/AstrBotDevs/AstrBot/pull/7764))
|
||||
- 微信客服文本消息新增 15 秒内去重,减少重复消息处理。([#7788](https://github.com/AstrBotDevs/AstrBot/pull/7788))
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化 Provider 配置界面性能与响应式显示,改善相关组件的字体和布局体验。([#7772](https://github.com/AstrBotDevs/AstrBot/pull/7772))
|
||||
- 优化统计页 TPM 计算逻辑,TPM 现在仅统计输出 Token,并更新相关文案。([#7827](https://github.com/AstrBotDevs/AstrBot/pull/7827))
|
||||
- 优化 OpenAI 兼容 Provider 的空 assistant 消息过滤逻辑,流式与非流式路径统一处理空字符串和空列表内容,避免严格 Provider 拒绝历史消息。([#7758](https://github.com/AstrBotDevs/AstrBot/pull/7758))
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 DeepSeek v4 与 reasoning content 相关处理,支持空字符串 reasoning 内容,并在 assistant 消息中保留 reasoning 字段。([#7823](https://github.com/AstrBotDevs/AstrBot/pull/7823), [#7830](https://github.com/AstrBotDevs/AstrBot/pull/7830))
|
||||
- 修复 OpenRouter reasoning 字段属性名不正确的问题。([#7821](https://github.com/AstrBotDevs/AstrBot/pull/7821))
|
||||
- 修复超大图片未压缩可能导致后续处理异常的问题,并复用图片最大尺寸检查工具。([#7807](https://github.com/AstrBotDevs/AstrBot/pull/7807))
|
||||
- 修复 MiniMax TTS 默认输出 MP3 导致 QQ 官方平台语音转换出现 RIFF 错误的问题,默认输出格式改为 WAV。([#7797](https://github.com/AstrBotDevs/AstrBot/pull/7797))
|
||||
- 修复 Computer 沙盒图片下载未按图片发送的问题。([#7785](https://github.com/AstrBotDevs/AstrBot/pull/7785))
|
||||
- 修复 Windows 环境下部分 HTTPS 请求证书校验失败的问题,使用 certifi SSL context 提升兼容性。([#7778](https://github.com/AstrBotDevs/AstrBot/pull/7778))
|
||||
- 修复非安全上下文或部分对话框中复制功能不可用的问题,抽取共享剪贴板工具并增加 fallback。([#7747](https://github.com/AstrBotDevs/AstrBot/pull/7747))
|
||||
- 修复文件上传可能存在路径穿越的问题,并清理上传文件名中的 NUL 字节。([#7751](https://github.com/AstrBotDevs/AstrBot/pull/7751))
|
||||
|
||||
<a id="english"></a>
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
|
||||
- Added a `/stats` command to show token usage for the current conversation, including total tokens, input cached tokens, input other tokens, and output tokens. ([#7831](https://github.com/AstrBotDevs/AstrBot/pull/7831))
|
||||
- Added Firecrawl web search and web extract tools with result handling, content extraction, session management, payload validation, and tests. ([#7764](https://github.com/AstrBotDevs/AstrBot/pull/7764))
|
||||
- Added 15-second deduplication for WeChat kefu text messages to reduce duplicate message handling. ([#7788](https://github.com/AstrBotDevs/AstrBot/pull/7788))
|
||||
|
||||
### Improvements
|
||||
|
||||
- Improved the Provider configuration UI performance and responsive layout, including font and component styling updates. ([#7772](https://github.com/AstrBotDevs/AstrBot/pull/7772))
|
||||
- Updated stats-page TPM calculation so TPM only counts output tokens, with matching label updates. ([#7827](https://github.com/AstrBotDevs/AstrBot/pull/7827))
|
||||
- Improved empty assistant message filtering for OpenAI-compatible providers by sharing the logic across streaming and non-streaming paths and handling empty string or empty list content. ([#7758](https://github.com/AstrBotDevs/AstrBot/pull/7758))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed DeepSeek v4 and reasoning content handling by supporting empty-string reasoning content and preserving the reasoning field in assistant messages. ([#7823](https://github.com/AstrBotDevs/AstrBot/pull/7823), [#7830](https://github.com/AstrBotDevs/AstrBot/pull/7830))
|
||||
- Fixed the reasoning field attribute used for OpenRouter. ([#7821](https://github.com/AstrBotDevs/AstrBot/pull/7821))
|
||||
- Fixed oversized image handling by downscaling large images and sharing the image max-size check helper. ([#7807](https://github.com/AstrBotDevs/AstrBot/pull/7807))
|
||||
- Fixed MiniMax TTS output for QQ Official voice conversion by changing the default output format from MP3 to WAV. ([#7797](https://github.com/AstrBotDevs/AstrBot/pull/7797))
|
||||
- Fixed Computer sandbox image downloads so they are sent as images. ([#7785](https://github.com/AstrBotDevs/AstrBot/pull/7785))
|
||||
- Fixed HTTPS certificate verification issues on Windows by using a certifi SSL context. ([#7778](https://github.com/AstrBotDevs/AstrBot/pull/7778))
|
||||
- Fixed copy actions in insecure contexts and dialogs by extracting a shared clipboard utility with fallback behavior. ([#7747](https://github.com/AstrBotDevs/AstrBot/pull/7747))
|
||||
- Fixed path traversal risks in file uploads and removed embedded NUL bytes from upload filenames. ([#7751](https://github.com/AstrBotDevs/AstrBot/pull/7751))
|
||||
@@ -293,6 +293,15 @@
|
||||
/>
|
||||
</template>
|
||||
<v-card class="stats-card" elevation="4">
|
||||
<div
|
||||
v-if="cachedInputTokens(messageContent(msg).agentStats) > 0"
|
||||
class="stats-row"
|
||||
>
|
||||
<span>{{ tm("stats.cachedTokens") }}</span>
|
||||
<strong>{{
|
||||
cachedInputTokens(messageContent(msg).agentStats)
|
||||
}}</strong>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<span>{{ tm("stats.inputTokens") }}</span>
|
||||
<strong>{{ inputTokens(messageContent(msg).agentStats) }}</strong>
|
||||
@@ -850,13 +859,17 @@ function formatTime(value: string) {
|
||||
|
||||
function inputTokens(stats: any) {
|
||||
const usage = stats?.token_usage || {};
|
||||
return (usage.input_other || 0) + (usage.input_cached || 0);
|
||||
return usage.input_other || 0;
|
||||
}
|
||||
|
||||
function outputTokens(stats: any) {
|
||||
return stats?.token_usage?.output || 0;
|
||||
}
|
||||
|
||||
function cachedInputTokens(stats: any) {
|
||||
return stats?.token_usage?.input_cached || 0;
|
||||
}
|
||||
|
||||
function agentDuration(stats: any) {
|
||||
const directDuration = readPositiveNumber(stats, [
|
||||
"duration",
|
||||
|
||||
@@ -185,6 +185,15 @@
|
||||
/>
|
||||
</template>
|
||||
<v-card class="stats-card" elevation="4">
|
||||
<div
|
||||
v-if="cachedInputTokens(messageContent(msg).agentStats) > 0"
|
||||
class="stats-row"
|
||||
>
|
||||
<span>{{ tm("stats.cachedTokens") }}</span>
|
||||
<strong>{{
|
||||
cachedInputTokens(messageContent(msg).agentStats)
|
||||
}}</strong>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<span>{{ tm("stats.inputTokens") }}</span>
|
||||
<strong>{{
|
||||
@@ -512,13 +521,17 @@ function formatTime(value: string) {
|
||||
|
||||
function inputTokens(stats: any) {
|
||||
const usage = stats?.token_usage || {};
|
||||
return (usage.input_other || 0) + (usage.input_cached || 0);
|
||||
return usage.input_other || 0;
|
||||
}
|
||||
|
||||
function outputTokens(stats: any) {
|
||||
return stats?.token_usage?.output || 0;
|
||||
}
|
||||
|
||||
function cachedInputTokens(stats: any) {
|
||||
return stats?.token_usage?.input_cached || 0;
|
||||
}
|
||||
|
||||
function agentDuration(stats: any) {
|
||||
const directDuration = readPositiveNumber(stats, [
|
||||
"duration",
|
||||
|
||||
@@ -137,9 +137,9 @@
|
||||
},
|
||||
"stats": {
|
||||
"tokens": "Tokens",
|
||||
"inputTokens": "Input Tokens",
|
||||
"inputTokens": "Input (other)",
|
||||
"outputTokens": "Output Tokens",
|
||||
"cachedTokens": "Cached Tokens",
|
||||
"cachedTokens": "Input (cached)",
|
||||
"duration": "Duration",
|
||||
"ttft": "Time to First Token"
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"callCount": "{count} calls",
|
||||
"avgTtft": "Average TTFT",
|
||||
"avgDuration": "Average Response Time",
|
||||
"avgTpm": "Average TPM",
|
||||
"avgTpm": "Average Output TPM",
|
||||
"successRate": "Success Rate"
|
||||
},
|
||||
"modelRanking": {
|
||||
|
||||
@@ -137,9 +137,9 @@
|
||||
},
|
||||
"stats": {
|
||||
"tokens": "Токены",
|
||||
"inputTokens": "Входящие",
|
||||
"inputTokens": "Входящие (прочие)",
|
||||
"outputTokens": "Исходящие",
|
||||
"cachedTokens": "Кэшированные",
|
||||
"cachedTokens": "Входящие (кэш)",
|
||||
"duration": "Время",
|
||||
"ttft": "Время до первого токена"
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"callCount": "{count} вызовов",
|
||||
"avgTtft": "Средний TTFT",
|
||||
"avgDuration": "Среднее время ответа",
|
||||
"avgTpm": "Средний TPM",
|
||||
"avgTpm": "Средний Output TPM",
|
||||
"successRate": "Доля успешных вызовов"
|
||||
},
|
||||
"modelRanking": {
|
||||
|
||||
@@ -137,9 +137,9 @@
|
||||
},
|
||||
"stats": {
|
||||
"tokens": "Token",
|
||||
"inputTokens": "输入 Token",
|
||||
"inputTokens": "输入(其他)",
|
||||
"outputTokens": "输出 Token",
|
||||
"cachedTokens": "缓存 Token",
|
||||
"cachedTokens": "输入(缓存)",
|
||||
"duration": "耗时",
|
||||
"ttft": "首字时间"
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"callCount": "共 {count} 次调用",
|
||||
"avgTtft": "平均首字延迟(TTFT)",
|
||||
"avgDuration": "平均响应时间",
|
||||
"avgTpm": "平均每分钟词元数(TPM)",
|
||||
"avgTpm": "平均每分钟输出(TPM)",
|
||||
"successRate": "调用成功率"
|
||||
},
|
||||
"modelRanking": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.23.5"
|
||||
version = "4.23.6"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
|
||||
Reference in New Issue
Block a user