Compare commits

..

2 Commits

Author SHA1 Message Date
Soulter a500f2edc8 chore: bump version to 4.13.1 2026-01-29 23:31:49 +08:00
Soulter d27099f2da fix(skills): update SANDBOX_SKILLS_ROOT path to use relative directory 2026-01-29 23:25:56 +08:00
28 changed files with 29 additions and 969 deletions
@@ -38,12 +38,7 @@ class ProcessLLMRequest:
req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
async def _ensure_persona(
self,
req: ProviderRequest,
cfg: dict,
umo: str,
platform_type: str,
event: AstrMessageEvent,
self, req: ProviderRequest, cfg: dict, umo: str, platform_type: str
):
"""确保用户人格已加载"""
if not req.conversation:
@@ -126,9 +121,6 @@ class ProcessLLMRequest:
req.func_tool = toolset
else:
req.func_tool.merge(toolset)
event.trace.record(
"sel_persona", persona_id=persona_id, persona_toolset=toolset.names()
)
logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
async def _ensure_img_caption(
@@ -233,7 +225,7 @@ class ProcessLLMRequest:
# inject persona for this request
platform_type = event.get_platform_name()
await self._ensure_persona(
req, cfg, event.unified_msg_origin, platform_type, event
req, cfg, event.unified_msg_origin, platform_type
)
# image caption
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.13.0"
__version__ = "4.13.1"
-2
View File
@@ -20,8 +20,6 @@ astrbot_config = AstrBotConfig()
t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img")
html_renderer = HtmlRenderer(t2i_base_url)
logger = LogManager.GetLogger(log_name="astrbot")
LogManager.configure_logger(logger, astrbot_config)
LogManager.configure_trace_logger(astrbot_config)
db_helper = SQLiteDatabase(DB_PATH)
# 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中
sp = SharedPreferences(db_helper=db_helper)
+1 -1
View File
@@ -84,7 +84,7 @@ class LocalPythonTool(FunctionTool):
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
) -> ToolExecResult:
if context.context.event.role != "admin":
return "error: Permission denied. Local Python execution is only allowed for admin users. Set admins in AstrBot WebUI."
return "error: Permission denied. Local Python execution is only allowed for admin users. Tell user to set admins in AstrBot WebUI."
sb = get_local_booter()
try:
+1 -1
View File
@@ -47,7 +47,7 @@ class ExecuteShellTool(FunctionTool):
env: dict = {},
) -> ToolExecResult:
if context.context.event.role != "admin":
return "error: Permission denied. Shell execution is only allowed for admin users. Set admins in AstrBot WebUI."
return "error: Permission denied. Shell execution is only allowed for admin users. Tell user to Set admins in AstrBot WebUI."
if self.is_local:
sb = get_local_booter()
+1 -49
View File
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.13.0"
VERSION = "4.13.1"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
WEBHOOK_SUPPORTED_PLATFORMS = [
@@ -182,12 +182,6 @@ DEFAULT_CONFIG = {
},
"wake_prefix": ["/"],
"log_level": "INFO",
"log_file_enable": False,
"log_file_path": "logs/astrbot.log",
"log_file_max_mb": 20,
"trace_log_enable": False,
"trace_log_path": "logs/astrbot.trace.log",
"trace_log_max_mb": 20,
"pip_install_arg": "",
"pypi_index_url": "https://mirrors.aliyun.com/pypi/simple/",
"persona": [], # deprecated
@@ -2327,18 +2321,6 @@ CONFIG_METADATA_2 = {
"type": "string",
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
},
"log_file_enable": {"type": "bool"},
"log_file_path": {"type": "string", "condition": {"log_file_enable": True}},
"log_file_max_mb": {"type": "int", "condition": {"log_file_enable": True}},
"trace_log_enable": {"type": "bool"},
"trace_log_path": {
"type": "string",
"condition": {"trace_log_enable": True},
},
"trace_log_max_mb": {
"type": "int",
"condition": {"trace_log_enable": True},
},
"t2i_strategy": {
"type": "string",
"options": ["remote", "local"],
@@ -3271,36 +3253,6 @@ CONFIG_METADATA_3_SYSTEM = {
"hint": "控制台输出日志的级别。",
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
},
"log_file_enable": {
"description": "启用文件日志",
"type": "bool",
"hint": "开启后会将日志写入指定文件。",
},
"log_file_path": {
"description": "日志文件路径",
"type": "string",
"hint": "相对路径以 data 目录为基准,例如 logs/astrbot.log;支持绝对路径。",
},
"log_file_max_mb": {
"description": "日志文件大小上限 (MB)",
"type": "int",
"hint": "超过大小后自动轮转,默认 20MB。",
},
"trace_log_enable": {
"description": "启用 Trace 文件日志",
"type": "bool",
"hint": "将 Trace 事件写入独立文件(不影响控制台输出)。",
},
"trace_log_path": {
"description": "Trace 日志文件路径",
"type": "string",
"hint": "相对路径以 data 目录为基准,例如 logs/astrbot.trace.log;支持绝对路径。",
},
"trace_log_max_mb": {
"description": "Trace 日志大小上限 (MB)",
"type": "int",
"hint": "超过大小后自动轮转,默认 20MB。",
},
"pip_install_arg": {
"description": "pip 安装额外参数",
"type": "string",
+3 -7
View File
@@ -17,7 +17,7 @@ import traceback
from asyncio import Queue
from astrbot.api import logger, sp
from astrbot.core import LogBroker, LogManager
from astrbot.core import LogBroker
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
from astrbot.core.config.default import VERSION
from astrbot.core.conversation_mgr import ConversationManager
@@ -80,13 +80,9 @@ class AstrBotCoreLifecycle:
# 初始化日志代理
logger.info("AstrBot v" + VERSION)
if os.environ.get("TESTING", ""):
LogManager.configure_logger(
logger, self.astrbot_config, override_level="DEBUG"
)
LogManager.configure_trace_logger(self.astrbot_config)
logger.setLevel("DEBUG") # 测试模式下设置日志级别为 DEBUG
else:
LogManager.configure_logger(logger, self.astrbot_config)
LogManager.configure_trace_logger(self.astrbot_config)
logger.setLevel(self.astrbot_config["log_level"]) # 设置日志级别
await self.db.initialize()
-1
View File
@@ -54,7 +54,6 @@ class EventBus:
event (AstrMessageEvent): 事件对象
"""
event.trace.record("event_dispatch", config_name=conf_name)
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
if event.get_sender_name():
logger.info(
+1 -189
View File
@@ -27,15 +27,13 @@ import sys
import time
from asyncio import Queue
from collections import deque
from logging.handlers import RotatingFileHandler
import colorlog
from astrbot.core.config.default import VERSION
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
# 日志缓存大小
CACHED_SIZE = 500
CACHED_SIZE = 200
# 日志颜色配置
log_color_config = {
"DEBUG": "green",
@@ -165,9 +163,6 @@ class LogManager:
提供了获取默认日志记录器logger和设置队列处理器的方法
"""
_FILE_HANDLER_FLAG = "_astrbot_file_handler"
_TRACE_FILE_HANDLER_FLAG = "_astrbot_trace_file_handler"
@classmethod
def GetLogger(cls, log_name: str = "default"):
"""获取指定名称的日志记录器logger
@@ -271,186 +266,3 @@ class LogManager:
),
)
logger.addHandler(handler)
@classmethod
def _default_log_path(cls) -> str:
return os.path.join(get_astrbot_data_path(), "logs", "astrbot.log")
@classmethod
def _resolve_log_path(cls, configured_path: str | None) -> str:
if not configured_path:
return cls._default_log_path()
if os.path.isabs(configured_path):
return configured_path
return os.path.join(get_astrbot_data_path(), configured_path)
@classmethod
def _get_file_handlers(cls, logger: logging.Logger) -> list[logging.Handler]:
return [
handler
for handler in logger.handlers
if getattr(handler, cls._FILE_HANDLER_FLAG, False)
]
@classmethod
def _get_trace_file_handlers(cls, logger: logging.Logger) -> list[logging.Handler]:
return [
handler
for handler in logger.handlers
if getattr(handler, cls._TRACE_FILE_HANDLER_FLAG, False)
]
@classmethod
def _remove_file_handlers(cls, logger: logging.Logger):
for handler in cls._get_file_handlers(logger):
logger.removeHandler(handler)
try:
handler.close()
except Exception:
pass
@classmethod
def _remove_trace_file_handlers(cls, logger: logging.Logger):
for handler in cls._get_trace_file_handlers(logger):
logger.removeHandler(handler)
try:
handler.close()
except Exception:
pass
@classmethod
def _add_file_handler(
cls,
logger: logging.Logger,
file_path: str,
max_mb: int | None = None,
backup_count: int = 3,
trace: bool = False,
):
os.makedirs(os.path.dirname(file_path) or ".", exist_ok=True)
max_bytes = 0
if max_mb and max_mb > 0:
max_bytes = max_mb * 1024 * 1024
if max_bytes > 0:
file_handler = RotatingFileHandler(
file_path,
maxBytes=max_bytes,
backupCount=backup_count,
encoding="utf-8",
)
else:
file_handler = logging.FileHandler(file_path, encoding="utf-8")
file_handler.setLevel(logger.level)
if trace:
formatter = logging.Formatter(
"[%(asctime)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
else:
formatter = logging.Formatter(
"[%(asctime)s] %(plugin_tag)s [%(short_levelname)s]%(astrbot_version_tag)s [%(filename)s:%(lineno)d]: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler.setFormatter(formatter)
setattr(
file_handler,
cls._TRACE_FILE_HANDLER_FLAG if trace else cls._FILE_HANDLER_FLAG,
True,
)
logger.addHandler(file_handler)
@classmethod
def configure_logger(
cls,
logger: logging.Logger,
config: dict | None,
override_level: str | None = None,
):
"""根据配置设置日志级别和文件日志。
Args:
logger: 需要配置的 logger
config: 配置字典
override_level: 若提供,将覆盖配置中的日志级别
"""
if not config:
return
level = override_level or config.get("log_level")
if level:
try:
logger.setLevel(level)
except Exception:
logger.setLevel(logging.INFO)
# 兼容旧版嵌套配置
if "log_file" in config:
file_conf = config.get("log_file") or {}
enable_file = bool(file_conf.get("enable", False))
file_path = file_conf.get("path")
max_mb = file_conf.get("max_mb")
else:
enable_file = bool(config.get("log_file_enable", False))
file_path = config.get("log_file_path")
max_mb = config.get("log_file_max_mb")
file_path = cls._resolve_log_path(file_path)
existing = cls._get_file_handlers(logger)
if not enable_file:
cls._remove_file_handlers(logger)
return
# 如果已有文件处理器且路径一致,则仅同步级别
if existing:
handler = existing[0]
base = getattr(handler, "baseFilename", "")
if base and os.path.abspath(base) == os.path.abspath(file_path):
handler.setLevel(logger.level)
return
cls._remove_file_handlers(logger)
cls._add_file_handler(logger, file_path, max_mb=max_mb)
@classmethod
def configure_trace_logger(cls, config: dict | None):
"""为 trace 事件配置独立的文件日志,不向控制台输出。"""
if not config:
return
enable = bool(
config.get("trace_log_enable")
or (config.get("log_file", {}) or {}).get("trace_enable", False)
)
path = config.get("trace_log_path")
max_mb = config.get("trace_log_max_mb")
if "log_file" in config:
legacy = config.get("log_file") or {}
path = path or legacy.get("trace_path")
max_mb = max_mb or legacy.get("trace_max_mb")
if not enable:
trace_logger = logging.getLogger("astrbot.trace")
cls._remove_trace_file_handlers(trace_logger)
return
file_path = cls._resolve_log_path(path or "logs/astrbot.trace.log")
trace_logger = logging.getLogger("astrbot.trace")
trace_logger.setLevel(logging.INFO)
trace_logger.propagate = False
existing = cls._get_trace_file_handlers(trace_logger)
if existing:
handler = existing[0]
base = getattr(handler, "baseFilename", "")
if base and os.path.abspath(base) == os.path.abspath(file_path):
handler.setLevel(trace_logger.level)
return
cls._remove_trace_file_handlers(trace_logger)
cls._add_file_handler(
trace_logger,
file_path,
max_mb=max_mb,
trace=True,
)
@@ -582,9 +582,7 @@ class InternalAgentSubStage(Stage):
req.extra_user_content_parts.append(
TextPart(text=f"[Image Attachment: path {image_path}]")
)
elif isinstance(comp, File) and self.sandbox_cfg.get(
"enable", False
):
elif isinstance(comp, File):
file_path = await comp.get_file()
file_name = comp.name or os.path.basename(file_path)
req.extra_user_content_parts.append(
@@ -611,7 +609,10 @@ class InternalAgentSubStage(Stage):
logger.error(f"Error occurred while applying file extract: {e}")
if not req.prompt and not req.image_urls:
return
if not event.get_group_id() and req.extra_user_content_parts:
req.prompt = "<attachment>"
else:
return
# call event hook
if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
@@ -691,17 +692,6 @@ class InternalAgentSubStage(Stage):
if action_type == "live":
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
event.trace.record(
"astr_agent_prepare",
system_prompt=req.system_prompt,
tools=req.func_tool.names() if req.func_tool else [],
stream=streaming_response,
chat_provider={
"id": provider.provider_config.get("id", ""),
"model": provider.get_model(),
},
)
await agent_runner.reset(
provider=provider,
request=req,
@@ -806,20 +796,12 @@ class InternalAgentSubStage(Stage):
):
yield
final_resp = agent_runner.get_final_llm_resp()
event.trace.record(
"astr_agent_complete",
stats=agent_runner.stats.to_dict(),
resp=final_resp.completion_text if final_resp else None,
)
# 检查事件是否被停止,如果被停止则不保存历史记录
if not event.is_stopped():
await self._save_to_history(
event,
req,
final_resp,
agent_runner.get_final_llm_resp(),
agent_runner.run_context.messages,
agent_runner.stats,
)
-2
View File
@@ -85,6 +85,4 @@ class PipelineScheduler:
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
await event.send(None)
event.trace.record("event_end")
logger.debug("pipeline 执行完毕。")
@@ -4,7 +4,6 @@ import hashlib
import re
import uuid
from collections.abc import AsyncGenerator
from time import time
from typing import Any
from astrbot import logger
@@ -23,7 +22,6 @@ from astrbot.core.message.message_event_result import MessageChain, MessageEvent
from astrbot.core.platform.message_type import MessageType
from astrbot.core.provider.entities import ProviderRequest
from astrbot.core.utils.metrics import Metric
from astrbot.core.utils.trace import TraceSpan
from .astrbot_message import AstrBotMessage, Group
from .message_session import MessageSesion, MessageSession # noqa
@@ -61,21 +59,6 @@ class AstrMessageEvent(abc.ABC):
self._result: MessageEventResult | None = None
"""消息事件的结果"""
self.created_at = time()
"""事件创建时间(Unix timestamp)"""
self.trace = TraceSpan(
name="AstrMessageEvent",
umo=self.unified_msg_origin,
sender_name=self.get_sender_name(),
message_outline=self.get_message_outline(),
)
"""用于记录事件处理的 TraceSpan 对象"""
self.span = self.trace
"""事件级 TraceSpan(别名: span)"""
self.trace.record("umo", umo=self.unified_msg_origin)
self.trace.record("event_created", created_at=self.created_at)
self._has_send_oper = False
"""在此次事件中是否有过至少一次发送消息的操作"""
self.call_llm = False
+2 -1
View File
@@ -17,7 +17,8 @@ from astrbot.core.utils.astrbot_path import (
SKILLS_CONFIG_FILENAME = "skills.json"
DEFAULT_SKILLS_CONFIG: dict[str, dict] = {"skills": {}}
SANDBOX_SKILLS_ROOT = "/home/shared/skills"
# SANDBOX_SKILLS_ROOT = "/home/shared/skills"
SANDBOX_SKILLS_ROOT = "skills"
_SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
-73
View File
@@ -1,73 +0,0 @@
import json
import logging
import time
import uuid
from typing import Any
from astrbot import logger
from astrbot.core import LogManager, astrbot_config
from astrbot.core.log import LogQueueHandler
_cached_log_broker = None
_trace_logger = None
def _get_log_broker():
global _cached_log_broker
if _cached_log_broker is not None:
return _cached_log_broker
for handler in logger.handlers:
if isinstance(handler, LogQueueHandler):
_cached_log_broker = handler.log_broker
return _cached_log_broker
return None
def _get_trace_logger():
global _trace_logger
if _trace_logger is not None:
return _trace_logger
# 按配置初始化 trace 文件日志
LogManager.configure_trace_logger(astrbot_config)
_trace_logger = logging.getLogger("astrbot.trace")
return _trace_logger
class TraceSpan:
def __init__(
self,
name: str,
umo: str | None = None,
sender_name: str | None = None,
message_outline: str | None = None,
) -> None:
self.span_id = str(uuid.uuid4())
self.name = name
self.umo = umo
self.sender_name = sender_name
self.message_outline = message_outline
self.started_at = time.time()
def record(self, action: str, **fields: Any) -> None:
payload = {
"type": "trace",
"level": "TRACE",
"time": time.time(),
"span_id": self.span_id,
"name": self.name,
"umo": self.umo,
"sender_name": self.sender_name,
"message_outline": self.message_outline,
"action": action,
"fields": fields,
}
log_broker = _get_log_broker()
if log_broker:
log_broker.publish(payload)
else:
logger.info(f"[trace] {payload}")
trace_logger = _get_trace_logger()
if trace_logger and trace_logger.handlers:
trace_logger.info(json.dumps(payload, ensure_ascii=False))
+7
View File
@@ -0,0 +1,7 @@
## What's Changed
### fixes
- feat(chat): refactor message rendering and introduce ToolCallItem component
- fix(db): using lambda expression to ensure updated_at field ([#4730](https://github.com/AstrBotDevs/AstrBot/issues/4730))
- fix(skills): update SANDBOX_SKILLS_ROOT path to use relative directory
@@ -1,487 +0,0 @@
<script setup>
import axios from 'axios';
import { EventSourcePolyfill } from 'event-source-polyfill';
</script>
<template>
<div class="trace-wrapper">
<div class="trace-table" ref="scrollEl" :style="{ height: tableHeight }">
<div class="trace-row trace-header">
<div class="trace-cell time">Time</div>
<div class="trace-cell span">Event ID</div>
<div class="trace-cell umo">UMO</div>
<!-- <div class="trace-cell count">Records</div> -->
<!-- <div class="trace-cell last">Last</div> -->
<div class="trace-cell sender">Sender</div>
<div class="trace-cell outline">Outline</div>
<div class="trace-cell fields"></div>
</div>
<div class="trace-group" :class="{ highlight: highlightMap[event.span_id] }" v-for="event in events"
:key="event.span_id">
<div class="trace-row trace-event">
<div class="trace-cell time">{{ formatTime(event.first_time) }}</div>
<div class="trace-cell span" :title="event.span_id">
<div class="event-title">
{{ shortSpan(event.span_id) }}
</div>
</div>
<div class="trace-cell umo">{{ event.umo }}</div>
<!-- <div class="trace-cell count">
<div class="event-meta">{{ event.records.length }}</div>
</div> -->
<!-- <div class="trace-cell last">
<div class="event-meta">{{ formatTime(event.last_time) }}</div>
</div> -->
<div class="trace-cell sender">
<div class="event-sub" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{
event.sender_name || '-' }}</div>
</div>
<div class="trace-cell outline">
<div class="event-sub outline">{{ event.message_outline || '-' }}</div>
</div>
<div class="trace-cell fields event-controls">
<v-btn size="x-small" variant="text" color="primary" @click="toggleEvent(event.span_id)">
{{ event.collapsed ? 'Expand' : 'Collapse' }}
<span v-if="event.hasAgentPrepare" class="agent-dot" />
</v-btn>
</div>
</div>
<div class="trace-records" v-if="!event.collapsed">
<div class="trace-record" v-for="record in getVisibleRecords(event)" :key="record.key">
<div class="trace-record-time">{{ record.timeLabel }}</div>
<div class="trace-record-action">{{ record.action }}</div>
<pre class="trace-record-fields">{{ record.fieldsText }}</pre>
</div>
<div class="event-more" v-if="event.visibleCount < event.records.length">
<v-btn size="x-small" variant="tonal" color="primary" @click="showMore(event.span_id)">
Show more
</v-btn>
</div>
</div>
</div>
<div v-if="events.length === 0" class="trace-empty">No trace data yet.</div>
</div>
</div>
</template>
<script>
export default {
name: 'TraceDisplayer',
props: {
autoScroll: {
type: Boolean,
default: true
},
maxItems: {
type: Number,
default: 300
}
},
data() {
return {
events: [],
eventIndex: {},
highlightMap: {},
highlightTimers: {},
eventSource: null,
retryTimer: null,
retryAttempts: 0,
maxRetryAttempts: 10,
baseRetryDelay: 1000,
lastEventId: null,
tableHeight: 'auto'
};
},
async mounted() {
await this.fetchTraceHistory();
this.connectSSE();
this.updateTableHeight();
window.addEventListener('resize', this.updateTableHeight);
},
beforeUnmount() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryAttempts = 0;
window.removeEventListener('resize', this.updateTableHeight);
},
methods: {
updateTableHeight() {
this.$nextTick(() => {
const el = this.$refs.scrollEl;
if (!el || typeof window === 'undefined') return;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const offsetTop = el.getBoundingClientRect().top;
const height = Math.max(viewportHeight - offsetTop, 0);
this.tableHeight = `${height}px`;
});
},
async fetchTraceHistory() {
try {
const res = await axios.get('/api/log-history');
const logs = res.data?.data?.logs || [];
const traces = logs.filter((item) => item.type === 'trace');
this.processNewTraces(traces);
} catch (err) {
console.error('Failed to fetch trace history:', err);
}
},
connectSSE() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
const token = localStorage.getItem('token');
this.eventSource = new EventSourcePolyfill('/api/live-log', {
headers: {
Authorization: token ? `Bearer ${token}` : ''
},
heartbeatTimeout: 300000,
withCredentials: true
});
this.eventSource.onopen = () => {
this.retryAttempts = 0;
if (!this.lastEventId) {
this.fetchTraceHistory();
}
};
this.eventSource.onmessage = (event) => {
try {
if (event.lastEventId) {
this.lastEventId = event.lastEventId;
}
const payload = JSON.parse(event.data);
if (payload?.type !== 'trace') {
return;
}
this.processNewTraces([payload]);
} catch (e) {
console.error('Failed to parse trace payload:', e);
}
};
this.eventSource.onerror = (err) => {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
if (this.retryAttempts >= this.maxRetryAttempts) {
console.error('Trace stream reached max retry attempts.');
return;
}
const delay = Math.min(
this.baseRetryDelay * Math.pow(2, this.retryAttempts),
30000
);
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryTimer = setTimeout(async () => {
this.retryAttempts++;
if (!this.lastEventId) {
await this.fetchTraceHistory();
}
this.connectSSE();
}, delay);
};
},
processNewTraces(newTraces) {
if (!newTraces || newTraces.length === 0) return;
let hasUpdate = false;
const touched = new Set();
newTraces.forEach((trace) => {
if (!trace.span_id) return;
const recordKey = `${trace.time}-${trace.span_id}-${trace.action}`;
let event = this.eventIndex[trace.span_id];
if (!event) {
event = {
span_id: trace.span_id,
name: trace.name,
umo: trace.umo,
sender_name: trace.sender_name,
message_outline: trace.message_outline,
first_time: trace.time,
last_time: trace.time,
collapsed: true,
visibleCount: 20,
records: [],
hasAgentPrepare: trace.action === 'astr_agent_prepare'
};
this.eventIndex[trace.span_id] = event;
this.events.push(event);
hasUpdate = true;
}
const exists = event.records.some((item) => item.key === recordKey);
if (exists) return;
event.records.push({
time: trace.time,
action: trace.action,
fieldsText: this.formatFields(trace.fields),
timeLabel: this.formatTime(trace.time),
key: recordKey
});
if (trace.action === 'astr_agent_prepare') {
event.hasAgentPrepare = true;
}
if (!event.first_time || trace.time < event.first_time) {
event.first_time = trace.time;
}
if (!event.last_time || trace.time > event.last_time) {
event.last_time = trace.time;
}
if (!event.sender_name && trace.sender_name) {
event.sender_name = trace.sender_name;
}
if (!event.message_outline && trace.message_outline) {
event.message_outline = trace.message_outline;
}
touched.add(trace.span_id);
hasUpdate = true;
});
if (hasUpdate) {
this.events.forEach((event) => {
event.records.sort((a, b) => b.time - a.time);
});
this.events.sort((a, b) => b.first_time - a.first_time);
if (this.events.length > this.maxItems) {
const overflow = this.events.length - this.maxItems;
const removed = this.events.splice(this.maxItems, overflow);
removed.forEach((event) => {
delete this.eventIndex[event.span_id];
});
}
touched.forEach((spanId) => {
this.pulseEvent(spanId);
});
}
},
scrollToBottom() {
const el = this.$refs.scrollEl;
if (!el) return;
el.scrollTop = el.scrollHeight;
},
toggleEvent(spanId) {
const event = this.eventIndex[spanId];
if (!event) return;
event.collapsed = !event.collapsed;
},
showMore(spanId) {
const event = this.eventIndex[spanId];
if (!event) return;
event.visibleCount = Math.min(event.records.length, event.visibleCount + 20);
},
pulseEvent(spanId) {
if (!spanId) return;
if (this.highlightTimers[spanId]) {
clearTimeout(this.highlightTimers[spanId]);
}
this.highlightMap = { ...this.highlightMap, [spanId]: true };
const remove = setTimeout(() => {
const next = { ...this.highlightMap };
delete next[spanId];
this.highlightMap = next;
const timers = { ...this.highlightTimers };
delete timers[spanId];
this.highlightTimers = timers;
}, 1200);
this.highlightTimers = { ...this.highlightTimers, [spanId]: remove };
},
getVisibleRecords(event) {
if (!event.records.length) return [];
return event.records.slice(0, event.visibleCount);
},
formatTime(ts) {
if (!ts) return '';
const date = new Date(ts * 1000);
const base = date.toLocaleString();
const ms = String(date.getMilliseconds()).padStart(3, '0');
return `${base}.${ms}`;
},
shortSpan(spanId) {
if (!spanId) return '';
return spanId.slice(0, 8);
},
formatFields(fields) {
if (!fields) return '';
try {
const text = JSON.stringify(fields, null, 2);
if (text.length > 2000) {
return `${text}`;
}
return text;
} catch (e) {
return String(fields);
}
}
}
};
</script>
<style scoped>
.trace-wrapper {
height: 100%;
}
.trace-table {
background: transparent;
border-radius: 0;
padding: 0;
height: 100%;
overflow-y: auto;
color: #2b3340;
font-family: 'Fira Code', monospace;
}
.trace-row {
display: grid;
grid-template-columns: 200px 100px 300px 90px 180px 140px 200px 1fr;
gap: 12px;
}
.trace-group {
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
background: transparent;
padding: 8px 0;
}
.trace-group.highlight {
background: rgba(59, 130, 246, 0.08);
transition: background 0.6s ease;
}
.trace-event {
align-items: start;
}
.trace-header {
font-weight: 600;
color: #6b7280;
border-bottom: 1px solid rgba(15, 23, 42, 0.12);
padding-bottom: 10px;
}
.trace-cell {
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
}
.event-title {
font-weight: 600;
color: #1f2937;
}
.event-meta {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.event-sub {
font-size: 12px;
color: #4b5563;
margin-top: 2px;
word-break: break-word;
}
.event-sub.outline {
color: #6b7280;
}
.event-controls {
display: flex;
justify-content: flex-end;
}
.agent-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
margin-left: 6px;
vertical-align: middle;
}
.trace-cell.fields pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: #4b5563;
}
.trace-empty {
padding: 24px;
text-align: center;
color: #6b7280;
}
@media (max-width: 1200px) {
.trace-row {
grid-template-columns: 140px 160px 300px 70px 140px 180px 1fr;
}
.trace-cell.fields {
grid-column: 1 / -1;
}
}
.trace-record {
display: grid;
grid-template-columns: 200px 120px 1fr;
gap: 8px;
padding: 2px 0;
}
.trace-record:last-child {
border-bottom: none;
}
.trace-record-time {
color: #6b7280;
font-size: 11px;
}
.trace-record-action {
color: #1f2937;
font-weight: 600;
font-size: 11px;
}
.trace-record-fields {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: #4b5563;
font-size: 10px;
}
.event-more {
display: flex;
justify-content: center;
padding: 6px 0 2px;
}
.trace-records {
padding: 4px 0 2px 0;
}
</style>
+1 -2
View File
@@ -46,7 +46,6 @@ export class I18nLoader {
{ name: 'features/config', path: 'features/config.json' },
{ name: 'features/config-metadata', path: 'features/config-metadata.json' },
{ name: 'features/console', path: 'features/console.json' },
{ name: 'features/trace', path: 'features/trace.json' },
{ name: 'features/about', path: 'features/about.json' },
{ name: 'features/settings', path: 'features/settings.json' },
{ name: 'features/auth', path: 'features/auth.json' },
@@ -296,4 +295,4 @@ export class I18nLoader {
}
}
}
@@ -11,7 +11,6 @@
"conversation": "Conversations",
"sessionManagement": "Custom Rules",
"console": "Console",
"trace": "Trace",
"alkaid": "Alkaid Lab",
"knowledgeBase": "Knowledge Base",
"about": "About",
@@ -564,30 +564,6 @@
"description": "Console Log Level",
"hint": "Log level for console output."
},
"log_file_enable": {
"description": "Enable File Logging",
"hint": "Write logs to a file in addition to the console."
},
"log_file_path": {
"description": "Log File Path",
"hint": "Relative paths are resolved under the data directory, e.g. logs/astrbot.log; absolute paths are supported."
},
"log_file_max_mb": {
"description": "Log File Max Size (MB)",
"hint": "Rotate when exceeding this size; default 20MB."
},
"trace_log_enable": {
"description": "Enable Trace File Logging",
"hint": "Write trace events to a separate file (does not change console output)."
},
"trace_log_path": {
"description": "Trace Log File Path",
"hint": "Relative paths are resolved under the data directory, e.g. logs/astrbot.trace.log; absolute paths are supported."
},
"trace_log_max_mb": {
"description": "Trace Log Max Size (MB)",
"hint": "Rotate when exceeding this size; default 20MB."
},
"pip_install_arg": {
"description": "Additional pip Installation Arguments",
"hint": "When installing plugin dependencies, Python's pip tool will be used. Additional arguments can be provided here, such as `--break-system-package`."
@@ -1,7 +0,0 @@
{
"title": "Trace",
"autoScroll": {
"enabled": "Auto-scroll: On",
"disabled": "Auto-scroll: Off"
}
}
@@ -11,7 +11,6 @@
"conversation": "对话数据",
"sessionManagement": "自定义规则",
"console": "平台日志",
"trace": "追踪",
"alkaid": "Alkaid",
"knowledgeBase": "知识库",
"about": "关于",
@@ -31,4 +30,4 @@
"selectVersion": "选择版本",
"current": "当前"
}
}
}
@@ -562,30 +562,6 @@
"description": "控制台日志级别",
"hint": "控制台输出日志的级别。"
},
"log_file_enable": {
"description": "启用文件日志",
"hint": "在控制台输出的同时,将日志写入文件。"
},
"log_file_path": {
"description": "日志文件路径",
"hint": "相对路径以 data 目录为基准,例如 logs/astrbot.log;支持绝对路径。"
},
"log_file_max_mb": {
"description": "日志文件大小上限 (MB)",
"hint": "超过大小后自动轮转,默认 20MB。"
},
"trace_log_enable": {
"description": "启用 Trace 文件日志",
"hint": "将 Trace 事件写入独立文件(不影响控制台输出)。"
},
"trace_log_path": {
"description": "Trace 日志文件路径",
"hint": "相对路径以 data 目录为基准,例如 logs/astrbot.trace.log;支持绝对路径。"
},
"trace_log_max_mb": {
"description": "Trace 日志大小上限 (MB)",
"hint": "超过大小后自动轮转,默认 20MB。"
},
"pip_install_arg": {
"description": "pip 安装额外参数",
"hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。"
@@ -1,7 +0,0 @@
{
"title": "追踪",
"autoScroll": {
"enabled": "自动滚动:开",
"disabled": "自动滚动:关"
}
}
+1 -5
View File
@@ -19,7 +19,6 @@ import zhCNPlatform from './locales/zh-CN/features/platform.json';
import zhCNConfig from './locales/zh-CN/features/config.json';
import zhCNConfigMetadata from './locales/zh-CN/features/config-metadata.json';
import zhCNConsole from './locales/zh-CN/features/console.json';
import zhCNTrace from './locales/zh-CN/features/trace.json';
import zhCNAbout from './locales/zh-CN/features/about.json';
import zhCNSettings from './locales/zh-CN/features/settings.json';
import zhCNAuth from './locales/zh-CN/features/auth.json';
@@ -57,7 +56,6 @@ import enUSPlatform from './locales/en-US/features/platform.json';
import enUSConfig from './locales/en-US/features/config.json';
import enUSConfigMetadata from './locales/en-US/features/config-metadata.json';
import enUSConsole from './locales/en-US/features/console.json';
import enUSTrace from './locales/en-US/features/trace.json';
import enUSAbout from './locales/en-US/features/about.json';
import enUSSettings from './locales/en-US/features/settings.json';
import enUSAuth from './locales/en-US/features/auth.json';
@@ -99,7 +97,6 @@ export const translations = {
config: zhCNConfig,
'config-metadata': zhCNConfigMetadata,
console: zhCNConsole,
trace: zhCNTrace,
about: zhCNAbout,
settings: zhCNSettings,
auth: zhCNAuth,
@@ -145,7 +142,6 @@ export const translations = {
config: enUSConfig,
'config-metadata': enUSConfigMetadata,
console: enUSConsole,
trace: enUSTrace,
about: enUSAbout,
settings: enUSSettings,
auth: enUSAuth,
@@ -173,4 +169,4 @@ export const translations = {
}
};
export type TranslationData = typeof translations;
export type TranslationData = typeof translations;
@@ -72,11 +72,6 @@ const sidebarItem: menu[] = [
icon: 'mdi-console',
to: '/console'
},
{
title: 'core.navigation.trace',
icon: 'mdi-timeline-text-outline',
to: '/trace'
},
]
}
// {
-5
View File
@@ -61,11 +61,6 @@ const MainRoutes = {
path: '/console',
component: () => import('@/views/ConsolePage.vue')
},
{
name: 'Trace',
path: '/trace',
component: () => import('@/views/TracePage.vue')
},
{
name: 'NativeKnowledgeBase',
path: '/knowledge-base',
-21
View File
@@ -1,21 +0,0 @@
<script setup>
import TraceDisplayer from '@/components/shared/TraceDisplayer.vue';
import { useModuleI18n } from '@/i18n/composables';
const { tm } = useModuleI18n('features/trace');
</script>
<template>
<div style="height: 100%;">
<TraceDisplayer />
</div>
</template>
<script>
export default {
name: 'TracePage',
components: {
TraceDisplayer
}
};
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
version = "4.13.0"
version = "4.13.1"
description = "Easy-to-use multi-platform LLM chatbot and development framework"
readme = "README.md"
requires-python = ">=3.10"