mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-16 15:06:04 +08:00
feat: show independent Agent and command trace types
This commit is contained in:
@@ -71,6 +71,7 @@ class TraceSpan:
|
||||
"has_errors": False,
|
||||
"has_models": False,
|
||||
"has_plugins": False,
|
||||
"has_commands": False,
|
||||
"span_count": 1,
|
||||
"truncated": False,
|
||||
"attributes": bounded_attributes(
|
||||
@@ -265,6 +266,8 @@ class TraceSpan:
|
||||
self._summary["span_count"] = self._span_count
|
||||
if action == "handler_start":
|
||||
self._summary["has_plugins"] = True
|
||||
if fields.get("handler_category") == "command":
|
||||
self._summary["has_commands"] = True
|
||||
if len(self._plugins) < MAX_SPANS:
|
||||
self._plugins.add(
|
||||
bounded_text(str(fields.get("plugin") or ""), 256)
|
||||
|
||||
@@ -493,8 +493,31 @@ class TraceStore:
|
||||
+ " ORDER BY start_ns DESC, trace_id DESC LIMIT ? OFFSET ?",
|
||||
[*values, limit + 1, (page - 1) * limit],
|
||||
).fetchall()
|
||||
summaries = [json.loads(row[0]) for row in rows[:limit]]
|
||||
# Older summaries predate command flags; derive only those flags
|
||||
# from this page's indexed spans without loading full trace detail.
|
||||
legacy = {
|
||||
summary["trace_id"]: summary
|
||||
for summary in summaries
|
||||
if "has_commands" not in summary
|
||||
}
|
||||
if legacy:
|
||||
placeholders = ",".join("?" for _ in legacy)
|
||||
for trace_id, data in connection.execute(
|
||||
f"SELECT trace_id, data FROM spans WHERE trace_id IN ({placeholders})",
|
||||
list(legacy),
|
||||
):
|
||||
if (
|
||||
json.loads(data)
|
||||
.get("attributes", {})
|
||||
.get("astrbot.handler_category")
|
||||
== "command"
|
||||
):
|
||||
legacy[trace_id]["has_commands"] = True
|
||||
for summary in legacy.values():
|
||||
summary.setdefault("has_commands", False)
|
||||
return {
|
||||
"traces": [json.loads(row[0]) for row in rows[:limit]],
|
||||
"traces": summaries,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"cursor": f"{rows[limit - 1][1]}:{rows[limit - 1][2]}"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Unreleased
|
||||
|
||||
- Add Type before UMO in the trace table, with independent blue Agent and green Command chips. Persist command flags in summaries and derive legacy flags for the current page; use explicit colors because the generic Vuetify color pack is disabled.
|
||||
|
||||
- Use flat status chips in the trace table and narrow the platform column to 80px, retaining a non-wrapping header.
|
||||
|
||||
- Set trace table header and cell text to 13px without changing timeline text or table density.
|
||||
|
||||
@@ -158,6 +158,7 @@ const traceHeaders = computed(() => [
|
||||
{ title: tm("columns.timestamp"), key: "timestamp", width: 160 },
|
||||
{ title: tm("columns.duration"), key: "duration", width: 120 },
|
||||
{ title: tm("columns.message"), key: "message", minWidth: 240 },
|
||||
{ title: tm("columns.category"), key: "category", width: 150, minWidth: 150, headerProps: { class: "text-no-wrap" } },
|
||||
{ title: "UMO", key: "umo", minWidth: 240 },
|
||||
{ title: tm("columns.sender"), key: "sender", minWidth: 120 },
|
||||
{ title: "Spans", key: "spans", width: 80 },
|
||||
@@ -333,6 +334,9 @@ function normalizeTracePayload(payload) {
|
||||
last_time: Number(summary.end_time_unix_nano) / 1e9 || time,
|
||||
completed: finished, status: summary.has_errors ? "error" : finished ? "success" : "running",
|
||||
has_models: summary.has_models, has_plugins: summary.has_plugins,
|
||||
has_commands: summary.has_commands ?? (payload.spans
|
||||
? payload.spans.some((span) => span.attributes?.["astrbot.handler_category"] === "command")
|
||||
: undefined),
|
||||
span_count: summary.span_count, truncated: summary.truncated,
|
||||
model_updated_ns: String(summary.updated_time_unix_nano || summary.start_time_unix_nano),
|
||||
};
|
||||
@@ -874,6 +878,12 @@ onBeforeUnmount(() => {
|
||||
<template #item.message="{ item: { event } }">
|
||||
<span class="trace-cell-text" :title="event.message_outline || tm('untitled')">{{ event.message_outline || tm("untitled") }}</span>
|
||||
</template>
|
||||
<template #item.category="{ item: { event } }">
|
||||
<div class="trace-category-chips">
|
||||
<v-chip v-if="matchesFilter(event, 'models')" size="small" variant="flat" color="#1976D2">{{ tm("classification.agent") }}</v-chip>
|
||||
<v-chip v-if="event.has_commands ?? event.records.some((record) => record.kind === 'plugin_handler' && record.fields.handler_category === 'command')" size="small" variant="flat" color="#2E7D32">{{ tm("classification.command") }}</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.umo="{ item: { event } }">
|
||||
<v-tooltip :text="event.umo" location="top">
|
||||
<template #activator="{ props }">
|
||||
@@ -1086,6 +1096,7 @@ onBeforeUnmount(() => {
|
||||
.trace-list-table :deep(.trace-list-row.selected) { background: rgba(var(--v-theme-primary), 0.1); }
|
||||
.trace-list-table :deep(.trace-list-row:focus-visible) { outline: 2px solid rgb(var(--v-theme-primary)); outline-offset: -2px; }
|
||||
.trace-cell-text { display: block; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.trace-category-chips { align-items: center; display: flex; flex-wrap: nowrap; gap: 6px; }
|
||||
.trace-item-platform { align-items: center; color: var(--trace-muted); display: inline-flex; height: 20px; justify-content: center; vertical-align: middle; width: 20px; }
|
||||
.trace-item-platform img { height: 20px; object-fit: contain; width: 20px; }
|
||||
.trace-detail { background: rgb(var(--v-theme-surface)); flex: 0 0 58%; max-width: 850px; min-height: 0; min-width: 360px; overflow: auto; padding: 18px 22px 28px; }
|
||||
|
||||
@@ -64,7 +64,12 @@
|
||||
"loadFailed": "Failed to load trace settings. Refresh to retry.",
|
||||
"updateFailed": "Failed to change trace recording. Please retry."
|
||||
},
|
||||
"classification": {
|
||||
"agent": "Agent",
|
||||
"command": "Command"
|
||||
},
|
||||
"columns": {
|
||||
"category": "Type",
|
||||
"duration": "Duration",
|
||||
"status": "Status",
|
||||
"platform": "Platform",
|
||||
|
||||
@@ -64,7 +64,12 @@
|
||||
"loadFailed": "トレース設定の取得に失敗しました。ページを更新してください。",
|
||||
"updateFailed": "トレース記録の切り替えに失敗しました。再試行してください。"
|
||||
},
|
||||
"classification": {
|
||||
"agent": "Agent",
|
||||
"command": "コマンド"
|
||||
},
|
||||
"columns": {
|
||||
"category": "種類",
|
||||
"duration": "実行時間",
|
||||
"status": "状態",
|
||||
"platform": "プラットフォーム",
|
||||
|
||||
@@ -64,7 +64,12 @@
|
||||
"loadFailed": "Не удалось загрузить настройки трассировки. Обновите страницу.",
|
||||
"updateFailed": "Не удалось изменить запись трассировок. Повторите попытку."
|
||||
},
|
||||
"classification": {
|
||||
"agent": "Agent",
|
||||
"command": "Команда"
|
||||
},
|
||||
"columns": {
|
||||
"category": "Тип",
|
||||
"duration": "Длительность",
|
||||
"status": "Статус",
|
||||
"platform": "Платформа",
|
||||
|
||||
@@ -64,7 +64,12 @@
|
||||
"loadFailed": "读取追踪设置失败,请刷新页面重试",
|
||||
"updateFailed": "切换追踪记录失败,请重试"
|
||||
},
|
||||
"classification": {
|
||||
"agent": "Agent",
|
||||
"command": "指令"
|
||||
},
|
||||
"columns": {
|
||||
"category": "类型",
|
||||
"duration": "时长",
|
||||
"status": "状态",
|
||||
"platform": "平台",
|
||||
|
||||
@@ -207,7 +207,7 @@ test("Vuetify datatable keeps the requested columns and explicit detail selectio
|
||||
const state = setup(t);
|
||||
assert.deepEqual(Array.from(state.traceHeaders.value, (header) => header.title), [
|
||||
"columns.status", "columns.platform", "columns.timestamp",
|
||||
"columns.duration", "columns.message", "UMO", "columns.sender", "Spans",
|
||||
"columns.duration", "columns.message", "columns.category", "UMO", "columns.sender", "Spans",
|
||||
]);
|
||||
const platformHeader = state.traceHeaders.value.find((header) => header.key === "platform");
|
||||
assert.equal(platformHeader.width, 80);
|
||||
@@ -216,6 +216,9 @@ test("Vuetify datatable keeps the requested columns and explicit detail selectio
|
||||
assert.match(descriptor.template.content, /<v-chip size="small" variant="flat" :color="event.status/);
|
||||
const zh = JSON.parse(readFileSync(new URL("../src/i18n/locales/zh-CN/features/trace.json", import.meta.url), "utf8"));
|
||||
assert.equal(zh.columns.duration, "时长");
|
||||
assert.equal(zh.columns.category, "类型");
|
||||
assert.match(descriptor.template.content, /matchesFilter\(event, 'models'\)[^>]*color="#1976D2"/);
|
||||
assert.match(descriptor.template.content, /event.has_commands \?\?[^>]*color="#2E7D32"/);
|
||||
assert.match(descriptor.template.content, /<v-data-table/);
|
||||
assert.match(descriptor.template.content, /item-value="event.span_id"/);
|
||||
assert.match(descriptor.template.content, /<v-data-table-server/);
|
||||
@@ -224,7 +227,7 @@ test("Vuetify datatable keeps the requested columns and explicit detail selectio
|
||||
assert.match(descriptor.template.content, /:items-length="totalRecords"/);
|
||||
assert.doesNotMatch(descriptor.template.content, /hide-default-footer|history.loadMore/);
|
||||
assert.doesNotMatch(descriptor.template.content, /#item=/);
|
||||
for (const key of ["status", "platform", "timestamp", "duration", "message", "umo", "sender", "spans"]) {
|
||||
for (const key of ["status", "platform", "timestamp", "duration", "message", "category", "umo", "sender", "spans"]) {
|
||||
assert.match(descriptor.template.content, new RegExp("#item\\." + key + "="));
|
||||
}
|
||||
assert.match(descriptor.template.content, /@click:row="\(_, \{ item \}\) => selectEvent\(item.event.span_id\)"/);
|
||||
|
||||
@@ -172,6 +172,8 @@ The toolbar contains search, fullscreen, font-size controls, and `Install Pip pa
|
||||
|
||||
### Trace
|
||||
|
||||
The Type column precedes UMO. Blue Agent indicates an Agent call; green Command indicates a command-handler call. Both can appear independently on the same trace. Ordinary listeners and event hooks do not count as commands. Summaries provide these flags; legacy command flags are derived for the current page without opening each trace's details.
|
||||
|
||||
New spans identify calls with `astrbot.kind` and describe execution through timestamps and status. They no longer store `astrbot.action`, `astrbot.end_action`, or `astrbot.duration_ms`; duration is calculated from nanosecond timestamps and `astrbot.handler_type` remains. Existing trace history stays readable without rewriting stored records.
|
||||
|
||||
Long titles show two lines and received/sent message previews show four lines by default. Overflowing text offers Show full text / Collapse. This only folds the display; recorded content is not truncated.
|
||||
@@ -190,7 +192,7 @@ Configure the trace database path, capacity budget (MB), and retention days unde
|
||||
|
||||
The trace tab has no outer gray card; its toolbar and list sit directly on the page with only the necessary separators. The former hint and switch are replaced by a toolbar button showing Pause (enabled and connected), Connecting (enabled but disconnected), or Start (disabled). Filters, recording, and refresh use matching light-gray icon-and-text buttons sharing the input's background color, all 36px high. Search fills the remaining width, pushing actions to the right. Its placeholder includes the message count instead of a separate counter. Click to pause or resume. Updates show loading and prevent repeated clicks; failures preserve the previous state and display an error. Pausing retains the list and open details in both themes.
|
||||
|
||||
The `Trace` tab shows message execution chains: receipt, plugin handlers, Agent requests, responses, and completion. It opens as a full-width Vuetify datatable with columns ordered as status chip, platform logo, timestamp, duration, message, UMO, sender, and Spans. Header and cell text uses 13px, while row height and spacing use Vuetify defaults; only cell content such as status, platform icons, and messages is customized. Status chips use the flat variant; the platform column is 80px wide with a non-wrapping header. Duration updates while execution is active. Click a row to open its timeline details on the right; use the close button or Escape to return to the list. History loading, incoming records, and filtering never automatically open another trace. Scroll horizontally on narrow screens. Spans counts actual spans including the message root, excluding span events; collapsing does not change the count. The search placeholder shows the total message traces matching the current filters. History is searched, filtered, and paginated server-side. Vuetify's datatable pager replaces Load more, with 50 rows by default, options of 25/50/100, and previous/next and first/last page controls. The browser retains only the current page's summaries and loads full detail only for the selected trace, releasing it on close. Search/filter changes return to page one. New records refresh the first page without inserting into historical pages. Open `Filters` next to search to select `Has errors`, `Has model calls`, and `Has plugin calls`. Multiple selections require all conditions and can be combined with search. No selection shows all traces; `Reset` clears selections, and closing the sidebar preserves them. This sidebar replaces the former `All / Errors / Agent / Plugins` single-select tabs. Use the toolbar recording button to enable or disable trace recording.
|
||||
The `Trace` tab shows message execution chains: receipt, plugin handlers, Agent requests, responses, and completion. It opens as a full-width Vuetify datatable with columns ordered as status chip, platform logo, timestamp, duration, message, type, UMO, sender, and Spans. Header and cell text uses 13px, while row height and spacing use Vuetify defaults; only cell content such as status, platform icons, and messages is customized. Status chips use the flat variant; the platform column is 80px wide with a non-wrapping header. Duration updates while execution is active. Click a row to open its timeline details on the right; use the close button or Escape to return to the list. History loading, incoming records, and filtering never automatically open another trace. Scroll horizontally on narrow screens. Spans counts actual spans including the message root, excluding span events; collapsing does not change the count. The search placeholder shows the total message traces matching the current filters. History is searched, filtered, and paginated server-side. Vuetify's datatable pager replaces Load more, with 50 rows by default, options of 25/50/100, and previous/next and first/last page controls. The browser retains only the current page's summaries and loads full detail only for the selected trace, releasing it on close. Search/filter changes return to page one. New records refresh the first page without inserting into historical pages. Open `Filters` next to search to select `Has errors`, `Has model calls`, and `Has plugin calls`. Multiple selections require all conditions and can be combined with search. No selection shows all traces; `Reset` clears selections, and closing the sidebar preserves them. This sidebar replaces the former `All / Errors / Agent / Plugins` single-select tabs. Use the toolbar recording button to enable or disable trace recording.
|
||||
|
||||
Session aliases replace raw UMOs when available; hover over the dotted underline to see the full UMO. Plugins use their display name when available; hover to see the plugin ID. Handler names and duration are also shown. Listener nodes omit the redundant `AdapterMessageEvent` type; other hook event types remain visible. Plugin handler nodes use the plugin logo when available and the plugin card's default blue star icon if it is missing or fails to load. Neutral chips next to the title show the category, matching the plugin detail page: command/command group, event listener, event hook, or LLM tool.
|
||||
|
||||
|
||||
@@ -172,6 +172,8 @@ ChatUI 支持以下常用能力:
|
||||
|
||||
### 追踪
|
||||
|
||||
UMO 前新增“类型”列:蓝色 Agent 表示 Agent 调用,绿色“指令”表示指令处理器调用。同一追踪可同时显示两个标签;普通监听器和事件钩子不算作指令。类型由追踪摘要提供,旧记录的指令标记按当前页数据推导,无需逐条打开详情。
|
||||
|
||||
新 Span 使用 `astrbot.kind` 表示调用类型,起止时间和状态表示执行结果;不再存储 `astrbot.action`、`astrbot.end_action` 或 `astrbot.duration_ms`。耗时由纳秒起止时间计算,`astrbot.handler_type` 保留。旧追踪仍可读取,历史数据库记录不会自动重写。
|
||||
|
||||
长消息标题默认显示 2 行,收到消息和发送响应的正文默认显示 4 行;超出时可点击“展开全文 / 收起”。这仅折叠界面展示,不截断已记录内容。
|
||||
@@ -190,7 +192,7 @@ Agent 默认折叠内部执行链,点击“Agent 执行”可展开请求准
|
||||
|
||||
追踪标签页不使用外层灰色卡片,工具栏和列表直接铺在页面上,仅保留必要的分隔线。原来的顶部说明和独立记录开关已移除;工具栏按钮显示“暂停”(启用且已连接)、“连接中”(启用但未连接)或“启动”(停用)。过滤器、记录按钮、刷新均采用图标加文字的浅灰按钮,与搜索框共用背景色,统一为 36px 高;搜索框填满剩余宽度,将操作按钮靠右排列。记录数量并入占位文字:“在 X 条记录中搜索消息、会话、插件”,不单独显示。点击记录按钮可暂停或恢复,更新期间显示加载状态并禁用重复点击;失败时保持原状态并提示错误。暂停不会清除已有列表或关闭详情,明暗主题保持相同布局。
|
||||
|
||||
`追踪` 页面实时显示消息执行链:收到消息、触发插件处理器、Agent 相关请求、发送响应和完成。默认只展示全宽 Vuetify datatable 追踪列表,列顺序为状态标签(运行中/已完成/错误)、平台 logo、时间戳、时长、消息、UMO、发送人和 Spans;表头和单元格字体为 13px,行高和间距采用 Vuetify 默认样式,仅定制状态、平台图标及消息等单元格内容。状态标签使用 flat 样式;平台列宽度为 80px,表头不会换行。时长在执行期间持续更新。点击某行后才打开右侧时间线详情,点击关闭按钮或按 Escape 返回列表;加载历史记录、新记录到达及筛选变化都不会自动打开其他记录的详情。窄屏列表可横向滚动。Spans 统计实际 Span 数(包括消息根 Span),不包含挂载在 Span 上的事件,折叠不改变计数;搜索占位文字显示满足当前条件的消息 trace 总数。历史记录在服务端搜索、筛选和分页;原来的“加载更多”改为 Vuetify datatable 分页器,默认每页 50 条,可选 25、50、100 条,并支持前后翻页和首尾页。浏览器只保留当前页摘要,只有当前打开的 trace 加载完整详情,关闭后释放详情。搜索或筛选会返回第一页;新记录只实时刷新第一页,不打乱正在查看的历史页。点击搜索框左侧的“过滤器”,勾选“有错误”“有模型调用”“有插件调用”;多选需同时满足所有条件,并可叠加搜索。不勾选则显示全部,“重置”清除勾选,关闭侧栏仍保留条件。原先的“全部 / 错误 / Agent / 插件”单选标签已改为多选侧栏。工具栏记录按钮可启用或禁用追踪记录。
|
||||
`追踪` 页面实时显示消息执行链:收到消息、触发插件处理器、Agent 相关请求、发送响应和完成。默认只展示全宽 Vuetify datatable 追踪列表,列顺序为状态标签(运行中/已完成/错误)、平台 logo、时间戳、时长、消息、类型、UMO、发送人和 Spans;表头和单元格字体为 13px,行高和间距采用 Vuetify 默认样式,仅定制状态、平台图标及消息等单元格内容。状态标签使用 flat 样式;平台列宽度为 80px,表头不会换行。时长在执行期间持续更新。点击某行后才打开右侧时间线详情,点击关闭按钮或按 Escape 返回列表;加载历史记录、新记录到达及筛选变化都不会自动打开其他记录的详情。窄屏列表可横向滚动。Spans 统计实际 Span 数(包括消息根 Span),不包含挂载在 Span 上的事件,折叠不改变计数;搜索占位文字显示满足当前条件的消息 trace 总数。历史记录在服务端搜索、筛选和分页;原来的“加载更多”改为 Vuetify datatable 分页器,默认每页 50 条,可选 25、50、100 条,并支持前后翻页和首尾页。浏览器只保留当前页摘要,只有当前打开的 trace 加载完整详情,关闭后释放详情。搜索或筛选会返回第一页;新记录只实时刷新第一页,不打乱正在查看的历史页。点击搜索框左侧的“过滤器”,勾选“有错误”“有模型调用”“有插件调用”;多选需同时满足所有条件,并可叠加搜索。不勾选则显示全部,“重置”清除勾选,关闭侧栏仍保留条件。原先的“全部 / 错误 / Agent / 插件”单选标签已改为多选侧栏。工具栏记录按钮可启用或禁用追踪记录。
|
||||
|
||||
UMO 有别名时显示别名,带点划线的文字可悬停查看完整 UMO。插件优先显示 display name,悬停可查看插件 ID,同时展示 Handler 名称及耗时。事件监听器不再重复显示底层类型 `AdapterMessageEvent`,其他事件钩子的类型仍保留。插件处理器节点优先使用插件 logo,没有 logo 或加载失败时使用插件卡片的默认蓝色星形图标;标题旁的中性标签展示与插件详情页一致的分类:指令/指令组、事件监听器、事件钩子或 LLM 工具。
|
||||
|
||||
|
||||
@@ -87,6 +87,53 @@ def test_standard_ids_parents_and_immutable_snapshots(capture):
|
||||
assert not trace._pending
|
||||
|
||||
|
||||
def test_agent_and_command_types_are_independent_and_persisted(persisted, capture):
|
||||
"""Keep listener hooks distinct while preserving both call classifications.
|
||||
|
||||
Args:
|
||||
persisted: Isolated trace database.
|
||||
capture: Immutable trace snapshots.
|
||||
"""
|
||||
trace = TraceSpan("message")
|
||||
with trace.context():
|
||||
trace.record(
|
||||
"handler_start",
|
||||
handler_full_name="plugin.listener",
|
||||
handler_category="listener",
|
||||
)
|
||||
trace.record(
|
||||
"handler_end", handler_full_name="plugin.listener", status="success"
|
||||
)
|
||||
assert trace._summary["has_commands"] is False
|
||||
trace.record(
|
||||
"handler_start",
|
||||
handler_full_name="plugin.command",
|
||||
handler_category="command",
|
||||
)
|
||||
trace.record(
|
||||
"handler_end", handler_full_name="plugin.command", status="success"
|
||||
)
|
||||
assert trace._summary["has_commands"] is True
|
||||
assert trace._summary["has_models"] is False
|
||||
trace.record("agent_start")
|
||||
trace.record("agent_end", status="success")
|
||||
trace.record("pipeline_end", status="success")
|
||||
for frame in capture:
|
||||
assert persisted.append(frame)
|
||||
assert persisted.flush()
|
||||
summary = persisted.list_traces()["traces"][0]
|
||||
assert summary["has_commands"] is True and summary["has_models"] is True
|
||||
with sqlite3.connect(persisted.path) as connection:
|
||||
summary.pop("has_commands")
|
||||
connection.execute(
|
||||
"UPDATE traces SET data=? WHERE trace_id=?",
|
||||
(json.dumps(summary), trace.trace_id),
|
||||
)
|
||||
legacy = persisted.list_traces()["traces"][0]
|
||||
assert legacy["has_commands"] is True
|
||||
assert persisted.get_trace(trace.trace_id)["trace"].get("has_commands") is None
|
||||
|
||||
|
||||
def test_internal_transitions_and_timing_are_not_persisted(capture):
|
||||
"""Keep call matching internal while preserving handler metadata.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user