Compare commits
3 Commits
master
...
feat/trace
| Author | SHA1 | Date | |
|---|---|---|---|
| eafb339281 | |||
| f03dd87502 | |||
| 6e475074a4 |
@@ -38,7 +38,12 @@ class ProcessLLMRequest:
|
|||||||
req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
|
req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
|
||||||
|
|
||||||
async def _ensure_persona(
|
async def _ensure_persona(
|
||||||
self, req: ProviderRequest, cfg: dict, umo: str, platform_type: str
|
self,
|
||||||
|
req: ProviderRequest,
|
||||||
|
cfg: dict,
|
||||||
|
umo: str,
|
||||||
|
platform_type: str,
|
||||||
|
event: AstrMessageEvent,
|
||||||
):
|
):
|
||||||
"""确保用户人格已加载"""
|
"""确保用户人格已加载"""
|
||||||
if not req.conversation:
|
if not req.conversation:
|
||||||
@@ -121,6 +126,9 @@ class ProcessLLMRequest:
|
|||||||
req.func_tool = toolset
|
req.func_tool = toolset
|
||||||
else:
|
else:
|
||||||
req.func_tool.merge(toolset)
|
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()}")
|
logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
|
||||||
|
|
||||||
async def _ensure_img_caption(
|
async def _ensure_img_caption(
|
||||||
@@ -225,7 +233,7 @@ class ProcessLLMRequest:
|
|||||||
# inject persona for this request
|
# inject persona for this request
|
||||||
platform_type = event.get_platform_name()
|
platform_type = event.get_platform_name()
|
||||||
await self._ensure_persona(
|
await self._ensure_persona(
|
||||||
req, cfg, event.unified_msg_origin, platform_type
|
req, cfg, event.unified_msg_origin, platform_type, event
|
||||||
)
|
)
|
||||||
|
|
||||||
# image caption
|
# image caption
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ astrbot_config = AstrBotConfig()
|
|||||||
t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img")
|
t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img")
|
||||||
html_renderer = HtmlRenderer(t2i_base_url)
|
html_renderer = HtmlRenderer(t2i_base_url)
|
||||||
logger = LogManager.GetLogger(log_name="astrbot")
|
logger = LogManager.GetLogger(log_name="astrbot")
|
||||||
|
LogManager.configure_logger(logger, astrbot_config)
|
||||||
|
LogManager.configure_trace_logger(astrbot_config)
|
||||||
db_helper = SQLiteDatabase(DB_PATH)
|
db_helper = SQLiteDatabase(DB_PATH)
|
||||||
# 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中
|
# 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中
|
||||||
sp = SharedPreferences(db_helper=db_helper)
|
sp = SharedPreferences(db_helper=db_helper)
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ DEFAULT_CONFIG = {
|
|||||||
},
|
},
|
||||||
"wake_prefix": ["/"],
|
"wake_prefix": ["/"],
|
||||||
"log_level": "INFO",
|
"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": "",
|
"pip_install_arg": "",
|
||||||
"pypi_index_url": "https://mirrors.aliyun.com/pypi/simple/",
|
"pypi_index_url": "https://mirrors.aliyun.com/pypi/simple/",
|
||||||
"persona": [], # deprecated
|
"persona": [], # deprecated
|
||||||
@@ -2321,6 +2327,18 @@ CONFIG_METADATA_2 = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
"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": {
|
"t2i_strategy": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"options": ["remote", "local"],
|
"options": ["remote", "local"],
|
||||||
@@ -3253,6 +3271,36 @@ CONFIG_METADATA_3_SYSTEM = {
|
|||||||
"hint": "控制台输出日志的级别。",
|
"hint": "控制台输出日志的级别。",
|
||||||
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
"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": {
|
"pip_install_arg": {
|
||||||
"description": "pip 安装额外参数",
|
"description": "pip 安装额外参数",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import traceback
|
|||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
|
|
||||||
from astrbot.api import logger, sp
|
from astrbot.api import logger, sp
|
||||||
from astrbot.core import LogBroker
|
from astrbot.core import LogBroker, LogManager
|
||||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||||
from astrbot.core.config.default import VERSION
|
from astrbot.core.config.default import VERSION
|
||||||
from astrbot.core.conversation_mgr import ConversationManager
|
from astrbot.core.conversation_mgr import ConversationManager
|
||||||
@@ -80,9 +80,13 @@ class AstrBotCoreLifecycle:
|
|||||||
# 初始化日志代理
|
# 初始化日志代理
|
||||||
logger.info("AstrBot v" + VERSION)
|
logger.info("AstrBot v" + VERSION)
|
||||||
if os.environ.get("TESTING", ""):
|
if os.environ.get("TESTING", ""):
|
||||||
logger.setLevel("DEBUG") # 测试模式下设置日志级别为 DEBUG
|
LogManager.configure_logger(
|
||||||
|
logger, self.astrbot_config, override_level="DEBUG"
|
||||||
|
)
|
||||||
|
LogManager.configure_trace_logger(self.astrbot_config)
|
||||||
else:
|
else:
|
||||||
logger.setLevel(self.astrbot_config["log_level"]) # 设置日志级别
|
LogManager.configure_logger(logger, self.astrbot_config)
|
||||||
|
LogManager.configure_trace_logger(self.astrbot_config)
|
||||||
|
|
||||||
await self.db.initialize()
|
await self.db.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class EventBus:
|
|||||||
event (AstrMessageEvent): 事件对象
|
event (AstrMessageEvent): 事件对象
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
event.trace.record("event_dispatch", config_name=conf_name)
|
||||||
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
||||||
if event.get_sender_name():
|
if event.get_sender_name():
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
+189
-1
@@ -27,13 +27,15 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
import colorlog
|
import colorlog
|
||||||
|
|
||||||
from astrbot.core.config.default import VERSION
|
from astrbot.core.config.default import VERSION
|
||||||
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
# 日志缓存大小
|
# 日志缓存大小
|
||||||
CACHED_SIZE = 200
|
CACHED_SIZE = 500
|
||||||
# 日志颜色配置
|
# 日志颜色配置
|
||||||
log_color_config = {
|
log_color_config = {
|
||||||
"DEBUG": "green",
|
"DEBUG": "green",
|
||||||
@@ -163,6 +165,9 @@ class LogManager:
|
|||||||
提供了获取默认日志记录器logger和设置队列处理器的方法
|
提供了获取默认日志记录器logger和设置队列处理器的方法
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_FILE_HANDLER_FLAG = "_astrbot_file_handler"
|
||||||
|
_TRACE_FILE_HANDLER_FLAG = "_astrbot_trace_file_handler"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def GetLogger(cls, log_name: str = "default"):
|
def GetLogger(cls, log_name: str = "default"):
|
||||||
"""获取指定名称的日志记录器logger
|
"""获取指定名称的日志记录器logger
|
||||||
@@ -266,3 +271,186 @@ class LogManager:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
logger.addHandler(handler)
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -691,6 +691,17 @@ class InternalAgentSubStage(Stage):
|
|||||||
if action_type == "live":
|
if action_type == "live":
|
||||||
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
|
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(
|
await agent_runner.reset(
|
||||||
provider=provider,
|
provider=provider,
|
||||||
request=req,
|
request=req,
|
||||||
@@ -795,12 +806,20 @@ class InternalAgentSubStage(Stage):
|
|||||||
):
|
):
|
||||||
yield
|
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():
|
if not event.is_stopped():
|
||||||
await self._save_to_history(
|
await self._save_to_history(
|
||||||
event,
|
event,
|
||||||
req,
|
req,
|
||||||
agent_runner.get_final_llm_resp(),
|
final_resp,
|
||||||
agent_runner.run_context.messages,
|
agent_runner.run_context.messages,
|
||||||
agent_runner.stats,
|
agent_runner.stats,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -85,4 +85,6 @@ class PipelineScheduler:
|
|||||||
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
|
||||||
await event.send(None)
|
await event.send(None)
|
||||||
|
|
||||||
|
event.trace.record("event_end")
|
||||||
|
|
||||||
logger.debug("pipeline 执行完毕。")
|
logger.debug("pipeline 执行完毕。")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import hashlib
|
|||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from time import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
@@ -22,6 +23,7 @@ from astrbot.core.message.message_event_result import MessageChain, MessageEvent
|
|||||||
from astrbot.core.platform.message_type import MessageType
|
from astrbot.core.platform.message_type import MessageType
|
||||||
from astrbot.core.provider.entities import ProviderRequest
|
from astrbot.core.provider.entities import ProviderRequest
|
||||||
from astrbot.core.utils.metrics import Metric
|
from astrbot.core.utils.metrics import Metric
|
||||||
|
from astrbot.core.utils.trace import TraceSpan
|
||||||
|
|
||||||
from .astrbot_message import AstrBotMessage, Group
|
from .astrbot_message import AstrBotMessage, Group
|
||||||
from .message_session import MessageSesion, MessageSession # noqa
|
from .message_session import MessageSesion, MessageSession # noqa
|
||||||
@@ -59,6 +61,21 @@ class AstrMessageEvent(abc.ABC):
|
|||||||
self._result: MessageEventResult | None = None
|
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._has_send_oper = False
|
||||||
"""在此次事件中是否有过至少一次发送消息的操作"""
|
"""在此次事件中是否有过至少一次发送消息的操作"""
|
||||||
self.call_llm = False
|
self.call_llm = False
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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))
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
<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>
|
||||||
@@ -46,6 +46,7 @@ export class I18nLoader {
|
|||||||
{ name: 'features/config', path: 'features/config.json' },
|
{ name: 'features/config', path: 'features/config.json' },
|
||||||
{ name: 'features/config-metadata', path: 'features/config-metadata.json' },
|
{ name: 'features/config-metadata', path: 'features/config-metadata.json' },
|
||||||
{ name: 'features/console', path: 'features/console.json' },
|
{ name: 'features/console', path: 'features/console.json' },
|
||||||
|
{ name: 'features/trace', path: 'features/trace.json' },
|
||||||
{ name: 'features/about', path: 'features/about.json' },
|
{ name: 'features/about', path: 'features/about.json' },
|
||||||
{ name: 'features/settings', path: 'features/settings.json' },
|
{ name: 'features/settings', path: 'features/settings.json' },
|
||||||
{ name: 'features/auth', path: 'features/auth.json' },
|
{ name: 'features/auth', path: 'features/auth.json' },
|
||||||
@@ -295,4 +296,4 @@ export class I18nLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"conversation": "Conversations",
|
"conversation": "Conversations",
|
||||||
"sessionManagement": "Custom Rules",
|
"sessionManagement": "Custom Rules",
|
||||||
"console": "Console",
|
"console": "Console",
|
||||||
|
"trace": "Trace",
|
||||||
"alkaid": "Alkaid Lab",
|
"alkaid": "Alkaid Lab",
|
||||||
"knowledgeBase": "Knowledge Base",
|
"knowledgeBase": "Knowledge Base",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
|
|||||||
@@ -564,6 +564,30 @@
|
|||||||
"description": "Console Log Level",
|
"description": "Console Log Level",
|
||||||
"hint": "Log level for console output."
|
"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": {
|
"pip_install_arg": {
|
||||||
"description": "Additional pip Installation Arguments",
|
"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`."
|
"hint": "When installing plugin dependencies, Python's pip tool will be used. Additional arguments can be provided here, such as `--break-system-package`."
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Trace",
|
||||||
|
"autoScroll": {
|
||||||
|
"enabled": "Auto-scroll: On",
|
||||||
|
"disabled": "Auto-scroll: Off"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"conversation": "对话数据",
|
"conversation": "对话数据",
|
||||||
"sessionManagement": "自定义规则",
|
"sessionManagement": "自定义规则",
|
||||||
"console": "平台日志",
|
"console": "平台日志",
|
||||||
|
"trace": "追踪",
|
||||||
"alkaid": "Alkaid",
|
"alkaid": "Alkaid",
|
||||||
"knowledgeBase": "知识库",
|
"knowledgeBase": "知识库",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
@@ -30,4 +31,4 @@
|
|||||||
"selectVersion": "选择版本",
|
"selectVersion": "选择版本",
|
||||||
"current": "当前"
|
"current": "当前"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,6 +562,30 @@
|
|||||||
"description": "控制台日志级别",
|
"description": "控制台日志级别",
|
||||||
"hint": "控制台输出日志的级别。"
|
"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": {
|
"pip_install_arg": {
|
||||||
"description": "pip 安装额外参数",
|
"description": "pip 安装额外参数",
|
||||||
"hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。"
|
"hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "追踪",
|
||||||
|
"autoScroll": {
|
||||||
|
"enabled": "自动滚动:开",
|
||||||
|
"disabled": "自动滚动:关"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import zhCNPlatform from './locales/zh-CN/features/platform.json';
|
|||||||
import zhCNConfig from './locales/zh-CN/features/config.json';
|
import zhCNConfig from './locales/zh-CN/features/config.json';
|
||||||
import zhCNConfigMetadata from './locales/zh-CN/features/config-metadata.json';
|
import zhCNConfigMetadata from './locales/zh-CN/features/config-metadata.json';
|
||||||
import zhCNConsole from './locales/zh-CN/features/console.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 zhCNAbout from './locales/zh-CN/features/about.json';
|
||||||
import zhCNSettings from './locales/zh-CN/features/settings.json';
|
import zhCNSettings from './locales/zh-CN/features/settings.json';
|
||||||
import zhCNAuth from './locales/zh-CN/features/auth.json';
|
import zhCNAuth from './locales/zh-CN/features/auth.json';
|
||||||
@@ -56,6 +57,7 @@ import enUSPlatform from './locales/en-US/features/platform.json';
|
|||||||
import enUSConfig from './locales/en-US/features/config.json';
|
import enUSConfig from './locales/en-US/features/config.json';
|
||||||
import enUSConfigMetadata from './locales/en-US/features/config-metadata.json';
|
import enUSConfigMetadata from './locales/en-US/features/config-metadata.json';
|
||||||
import enUSConsole from './locales/en-US/features/console.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 enUSAbout from './locales/en-US/features/about.json';
|
||||||
import enUSSettings from './locales/en-US/features/settings.json';
|
import enUSSettings from './locales/en-US/features/settings.json';
|
||||||
import enUSAuth from './locales/en-US/features/auth.json';
|
import enUSAuth from './locales/en-US/features/auth.json';
|
||||||
@@ -97,6 +99,7 @@ export const translations = {
|
|||||||
config: zhCNConfig,
|
config: zhCNConfig,
|
||||||
'config-metadata': zhCNConfigMetadata,
|
'config-metadata': zhCNConfigMetadata,
|
||||||
console: zhCNConsole,
|
console: zhCNConsole,
|
||||||
|
trace: zhCNTrace,
|
||||||
about: zhCNAbout,
|
about: zhCNAbout,
|
||||||
settings: zhCNSettings,
|
settings: zhCNSettings,
|
||||||
auth: zhCNAuth,
|
auth: zhCNAuth,
|
||||||
@@ -142,6 +145,7 @@ export const translations = {
|
|||||||
config: enUSConfig,
|
config: enUSConfig,
|
||||||
'config-metadata': enUSConfigMetadata,
|
'config-metadata': enUSConfigMetadata,
|
||||||
console: enUSConsole,
|
console: enUSConsole,
|
||||||
|
trace: enUSTrace,
|
||||||
about: enUSAbout,
|
about: enUSAbout,
|
||||||
settings: enUSSettings,
|
settings: enUSSettings,
|
||||||
auth: enUSAuth,
|
auth: enUSAuth,
|
||||||
@@ -169,4 +173,4 @@ export const translations = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TranslationData = typeof translations;
|
export type TranslationData = typeof translations;
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ const sidebarItem: menu[] = [
|
|||||||
icon: 'mdi-console',
|
icon: 'mdi-console',
|
||||||
to: '/console'
|
to: '/console'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'core.navigation.trace',
|
||||||
|
icon: 'mdi-timeline-text-outline',
|
||||||
|
to: '/trace'
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ const MainRoutes = {
|
|||||||
path: '/console',
|
path: '/console',
|
||||||
component: () => import('@/views/ConsolePage.vue')
|
component: () => import('@/views/ConsolePage.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Trace',
|
||||||
|
path: '/trace',
|
||||||
|
component: () => import('@/views/TracePage.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'NativeKnowledgeBase',
|
name: 'NativeKnowledgeBase',
|
||||||
path: '/knowledge-base',
|
path: '/knowledge-base',
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<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>
|
||||||
Reference in New Issue
Block a user