From 6e475074a460072e0df4c9151c1481bfbeacd7db Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 29 Jan 2026 20:56:45 +0800 Subject: [PATCH] feat: trace --- .../astrbot/process_llm_request.py | 12 +- astrbot/core/event_bus.py | 1 + .../method/agent_sub_stages/internal.py | 21 +- astrbot/core/pipeline/scheduler.py | 2 + astrbot/core/platform/astr_message_event.py | 17 + astrbot/core/utils/trace.py | 54 +++ .../src/components/shared/TraceDisplayer.vue | 458 ++++++++++++++++++ dashboard/src/i18n/loader.ts | 3 +- .../i18n/locales/en-US/core/navigation.json | 1 + .../i18n/locales/en-US/features/trace.json | 7 + .../i18n/locales/zh-CN/core/navigation.json | 3 +- .../i18n/locales/zh-CN/features/trace.json | 7 + dashboard/src/i18n/translations.ts | 6 +- .../full/vertical-sidebar/sidebarItem.ts | 5 + dashboard/src/router/MainRoutes.ts | 5 + dashboard/src/views/TracePage.vue | 25 + 16 files changed, 621 insertions(+), 6 deletions(-) create mode 100644 astrbot/core/utils/trace.py create mode 100644 dashboard/src/components/shared/TraceDisplayer.vue create mode 100644 dashboard/src/i18n/locales/en-US/features/trace.json create mode 100644 dashboard/src/i18n/locales/zh-CN/features/trace.json create mode 100644 dashboard/src/views/TracePage.vue diff --git a/astrbot/builtin_stars/astrbot/process_llm_request.py b/astrbot/builtin_stars/astrbot/process_llm_request.py index 7475446ab..5a4ed5b1f 100644 --- a/astrbot/builtin_stars/astrbot/process_llm_request.py +++ b/astrbot/builtin_stars/astrbot/process_llm_request.py @@ -38,7 +38,12 @@ class ProcessLLMRequest: req.func_tool.add_tool(LOCAL_PYTHON_TOOL) 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: @@ -121,6 +126,9 @@ 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( @@ -225,7 +233,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 + req, cfg, event.unified_msg_origin, platform_type, event ) # image caption diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py index 0017e65fa..82675585a 100644 --- a/astrbot/core/event_bus.py +++ b/astrbot/core/event_bus.py @@ -54,6 +54,7 @@ class EventBus: event (AstrMessageEvent): 事件对象 """ + event.trace.record("event_dispatch", config_name=conf_name) # 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要 if event.get_sender_name(): logger.info( diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index 4c20b3025..9fe61eab4 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py @@ -691,6 +691,17 @@ 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, @@ -795,12 +806,20 @@ 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, - agent_runner.get_final_llm_resp(), + final_resp, agent_runner.run_context.messages, agent_runner.stats, ) diff --git a/astrbot/core/pipeline/scheduler.py b/astrbot/core/pipeline/scheduler.py index 8569f945a..6d2d72b80 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -85,4 +85,6 @@ class PipelineScheduler: if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent): await event.send(None) + event.trace.record("event_end") + logger.debug("pipeline 执行完毕。") diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 9c0418be8..a1bc393c7 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -4,6 +4,7 @@ import hashlib import re import uuid from collections.abc import AsyncGenerator +from time import time from typing import Any 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.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 @@ -59,6 +61,21 @@ 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 diff --git a/astrbot/core/utils/trace.py b/astrbot/core/utils/trace.py new file mode 100644 index 000000000..bbd05de42 --- /dev/null +++ b/astrbot/core/utils/trace.py @@ -0,0 +1,54 @@ +import time +import uuid +from typing import Any + +from astrbot import logger +from astrbot.core.log import LogQueueHandler + +_cached_log_broker = 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 + + +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) + return + logger.info(f"[trace] {payload}") diff --git a/dashboard/src/components/shared/TraceDisplayer.vue b/dashboard/src/components/shared/TraceDisplayer.vue new file mode 100644 index 000000000..6567c35ed --- /dev/null +++ b/dashboard/src/components/shared/TraceDisplayer.vue @@ -0,0 +1,458 @@ + + + + + + + Time + Event ID + UMO + + + Sender + Outline + + + + + {{ formatTime(event.first_time) }} + + + {{ shortSpan(event.span_id) }} + + + {{ event.umo }} + + + + {{ + event.sender_name || '-' }} + + + {{ event.message_outline || '-' }} + + + + {{ event.collapsed ? 'Expand' : 'Collapse' }} + + + + + + {{ record.timeLabel }} + {{ record.action }} + {{ record.fieldsText }} + + + + Show more + + + + + No trace data yet. + + + + + + + diff --git a/dashboard/src/i18n/loader.ts b/dashboard/src/i18n/loader.ts index ad17d58cb..5d39d0b64 100644 --- a/dashboard/src/i18n/loader.ts +++ b/dashboard/src/i18n/loader.ts @@ -46,6 +46,7 @@ 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' }, @@ -295,4 +296,4 @@ export class I18nLoader { } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/en-US/core/navigation.json b/dashboard/src/i18n/locales/en-US/core/navigation.json index 52f1eb110..5af29f3be 100644 --- a/dashboard/src/i18n/locales/en-US/core/navigation.json +++ b/dashboard/src/i18n/locales/en-US/core/navigation.json @@ -11,6 +11,7 @@ "conversation": "Conversations", "sessionManagement": "Custom Rules", "console": "Console", + "trace": "Trace", "alkaid": "Alkaid Lab", "knowledgeBase": "Knowledge Base", "about": "About", diff --git a/dashboard/src/i18n/locales/en-US/features/trace.json b/dashboard/src/i18n/locales/en-US/features/trace.json new file mode 100644 index 000000000..777dbe946 --- /dev/null +++ b/dashboard/src/i18n/locales/en-US/features/trace.json @@ -0,0 +1,7 @@ +{ + "title": "Trace", + "autoScroll": { + "enabled": "Auto-scroll: On", + "disabled": "Auto-scroll: Off" + } +} diff --git a/dashboard/src/i18n/locales/zh-CN/core/navigation.json b/dashboard/src/i18n/locales/zh-CN/core/navigation.json index 519de9c25..981f8a853 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/navigation.json +++ b/dashboard/src/i18n/locales/zh-CN/core/navigation.json @@ -11,6 +11,7 @@ "conversation": "对话数据", "sessionManagement": "自定义规则", "console": "平台日志", + "trace": "追踪", "alkaid": "Alkaid", "knowledgeBase": "知识库", "about": "关于", @@ -30,4 +31,4 @@ "selectVersion": "选择版本", "current": "当前" } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/trace.json b/dashboard/src/i18n/locales/zh-CN/features/trace.json new file mode 100644 index 000000000..5e36ac5d5 --- /dev/null +++ b/dashboard/src/i18n/locales/zh-CN/features/trace.json @@ -0,0 +1,7 @@ +{ + "title": "追踪", + "autoScroll": { + "enabled": "自动滚动:开", + "disabled": "自动滚动:关" + } +} diff --git a/dashboard/src/i18n/translations.ts b/dashboard/src/i18n/translations.ts index 8cff882be..dd67ca54a 100644 --- a/dashboard/src/i18n/translations.ts +++ b/dashboard/src/i18n/translations.ts @@ -19,6 +19,7 @@ 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'; @@ -56,6 +57,7 @@ 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'; @@ -97,6 +99,7 @@ export const translations = { config: zhCNConfig, 'config-metadata': zhCNConfigMetadata, console: zhCNConsole, + trace: zhCNTrace, about: zhCNAbout, settings: zhCNSettings, auth: zhCNAuth, @@ -142,6 +145,7 @@ export const translations = { config: enUSConfig, 'config-metadata': enUSConfigMetadata, console: enUSConsole, + trace: enUSTrace, about: enUSAbout, settings: enUSSettings, auth: enUSAuth, @@ -169,4 +173,4 @@ export const translations = { } }; -export type TranslationData = typeof translations; \ No newline at end of file +export type TranslationData = typeof translations; diff --git a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts index 3972dd9aa..e26d34957 100644 --- a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts +++ b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts @@ -72,6 +72,11 @@ const sidebarItem: menu[] = [ icon: 'mdi-console', to: '/console' }, + { + title: 'core.navigation.trace', + icon: 'mdi-timeline-text-outline', + to: '/trace' + }, ] } // { diff --git a/dashboard/src/router/MainRoutes.ts b/dashboard/src/router/MainRoutes.ts index 0a8617426..e4ca0ee77 100644 --- a/dashboard/src/router/MainRoutes.ts +++ b/dashboard/src/router/MainRoutes.ts @@ -61,6 +61,11 @@ const MainRoutes = { path: '/console', component: () => import('@/views/ConsolePage.vue') }, + { + name: 'Trace', + path: '/trace', + component: () => import('@/views/TracePage.vue') + }, { name: 'NativeKnowledgeBase', path: '/knowledge-base', diff --git a/dashboard/src/views/TracePage.vue b/dashboard/src/views/TracePage.vue new file mode 100644 index 000000000..d14eb0aa7 --- /dev/null +++ b/dashboard/src/views/TracePage.vue @@ -0,0 +1,25 @@ + + + + + + {{ tm('title') }} + + + + + +
{{ record.fieldsText }}