dsh-my-observability 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/lib/ai.js +6 -1
- package/lib/audit-view.js +18 -1
- package/lib/audit.js +97 -0
- package/lib/client.js +119 -10
- package/lib/parts/i18n.js +6 -0
- package/lib/parts/replay.js +70 -5
- package/lib/parts/styles.js +9 -0
- package/lib/store.js +16 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 dsh-my-observability 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.1.7] - 2026-09-06
|
|
6
|
+
|
|
7
|
+
### 变更
|
|
8
|
+
|
|
9
|
+
- fix(task-reliability,observability): #165 Session.events 迁移——snapshotEvents 优先 + 旧 API 兜底 (#166)
|
|
10
|
+
- feat(observability,task-reliability): #154 插件关键行为纳入事件审计(干预/ask 决策/救场/verify/恢复)+ 文件体积控制 (#158)
|
|
11
|
+
- fix(observability): #142 dispose 落盘链可等待,修复 release 门禁 flaky 测试 (#148)
|
|
12
|
+
|
|
13
|
+
## [0.1.6] - 2026-09-04
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
|
|
17
|
+
- fix(observability): 轨迹回放会话下拉可读标题(首条用户消息)——此前只展示 UUID 清单
|
|
18
|
+
|
|
5
19
|
## [0.1.5] - 2026-09-04
|
|
6
20
|
|
|
7
21
|
### 变更
|
package/lib/ai.js
CHANGED
|
@@ -127,9 +127,14 @@ function extractJsonBlock(text) {
|
|
|
127
127
|
return match !== null ? match[1].trim() : undefined
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** 会话事件快照:新 API snapshotEvents() 优先,旧 API events 兜底(issue #165)。 */
|
|
131
|
+
function sessionEvents(session) {
|
|
132
|
+
return session?.snapshotEvents?.() ?? session?.events
|
|
133
|
+
}
|
|
134
|
+
|
|
130
135
|
function lastAssistantText(session) {
|
|
131
136
|
try {
|
|
132
|
-
const events = session
|
|
137
|
+
const events = sessionEvents(session)
|
|
133
138
|
if (!Array.isArray(events)) return ''
|
|
134
139
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
135
140
|
const text = assistantTextOf(events[i])
|
package/lib/audit-view.js
CHANGED
|
@@ -75,12 +75,28 @@ function toolResultParts(data) {
|
|
|
75
75
|
return parts
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** 插件事件(issue #154)的搜索片段(插件名/事件名/动作/原因/参数值)。 */
|
|
79
|
+
function pluginEventParts(data) {
|
|
80
|
+
const parts = []
|
|
81
|
+
if (typeof data.plugin === 'string') parts.push(data.plugin)
|
|
82
|
+
if (typeof data.event === 'string') parts.push(data.event)
|
|
83
|
+
if (typeof data.action === 'string') parts.push(data.action)
|
|
84
|
+
if (typeof data.reason === 'string') parts.push(data.reason)
|
|
85
|
+
if (data.params !== null && typeof data.params === 'object') {
|
|
86
|
+
for (const value of Object.values(data.params)) {
|
|
87
|
+
if (typeof value === 'string' && value !== '') parts.push(value)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return parts
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
/** 事件类型 → 搜索片段收集函数(查表消分支)。 */
|
|
79
94
|
const PARTS_COLLECTORS = {
|
|
80
95
|
agent_status: agentStatusParts,
|
|
81
96
|
llm_stream: llmParts,
|
|
82
97
|
tool_call: toolCallParts,
|
|
83
98
|
tool_result: toolResultParts,
|
|
99
|
+
plugin_event: pluginEventParts,
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
/** 提取事件可用于关键词匹配的文本(工具名/参数摘要/错误信息/状态/阶段等)。 */
|
|
@@ -119,10 +135,11 @@ function normalizeCriteria(criteria) {
|
|
|
119
135
|
return { type: criteria.type ?? '', keyword: criteria.keyword ?? '', result: criteria.result ?? '', start, end }
|
|
120
136
|
}
|
|
121
137
|
|
|
122
|
-
/** 类型过滤('tool' 表示 tool_call + tool_result)。 */
|
|
138
|
+
/** 类型过滤('tool' 表示 tool_call + tool_result;'plugin' 表示 plugin_event)。 */
|
|
123
139
|
function passType(type, filterType) {
|
|
124
140
|
if (filterType === '') return true
|
|
125
141
|
if (filterType === 'tool') return type === 'tool_call' || type === 'tool_result'
|
|
142
|
+
if (filterType === 'plugin') return type === 'plugin_event'
|
|
126
143
|
return type === filterType
|
|
127
144
|
}
|
|
128
145
|
|
package/lib/audit.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* waterfall,包装流透传全部 chunk)
|
|
8
8
|
* - `tools/pre-execute` → `tool_call`(工具调用开始 + 参数摘要;透传 next)
|
|
9
9
|
* - `tools/execute` → `tool_result`(工具结果 ok/失败 + 耗时;透传 next)
|
|
10
|
+
* - 插件事件(issue #154)→ `plugin_event`(插件关键行为:干预/ask 决策/
|
|
11
|
+
* 救场/校验/恢复,含插件名/事件名/动作/原因/参数)
|
|
10
12
|
*
|
|
11
13
|
* ⚠️ llm/stream 监听器必须保持同步函数:cordis waterfall 不 await listener
|
|
12
14
|
* 返回值,next() 同步返回流;async listener 会让消费方(vision-toolkit 等
|
|
@@ -14,16 +16,111 @@
|
|
|
14
16
|
*/
|
|
15
17
|
import { MAX_ARG_KEYS, MAX_TEXT_LEN } from './constants.js'
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 采集的插件事件名(按插件前缀显式注册,不依赖 cordis 通配符行为)。
|
|
21
|
+
* 与 dsh-task-reliability/lib/emit.js 的 PLUGIN_EVENTS 保持一致;
|
|
22
|
+
* 未来推广到其他插件(dsh-my-guardian 等)在此追加。
|
|
23
|
+
*/
|
|
24
|
+
const PLUGIN_EVENT_NAMES = [
|
|
25
|
+
'task-reliability/intervention',
|
|
26
|
+
'task-reliability/ask-decision',
|
|
27
|
+
'task-reliability/rescue',
|
|
28
|
+
'task-reliability/verify',
|
|
29
|
+
'task-reliability/resume',
|
|
30
|
+
]
|
|
31
|
+
|
|
17
32
|
/** 注册全部审计监听;返回 disposer 数组(全部经 ctx.on 注册)。 */
|
|
18
33
|
export function attachAuditListeners(ctx, record) {
|
|
19
34
|
return [
|
|
35
|
+
ctx.on('session/event', (session, event) => handleSessionEvent(session, event, record)),
|
|
20
36
|
ctx.on('agent/status', (payload) => handleStatus(payload, record)),
|
|
21
37
|
ctx.on('llm/stream', (options, next) => handleStream(options, next, record)),
|
|
22
38
|
ctx.on('tools/pre-execute', (exec, next) => handlePreExecute(exec, next, record)),
|
|
23
39
|
ctx.on('tools/execute', (exec, next) => handleExecute(exec, next, record)),
|
|
40
|
+
...PLUGIN_EVENT_NAMES.map((name) => ctx.on(name, (payload) => handlePluginEvent(name, payload, record))),
|
|
24
41
|
]
|
|
25
42
|
}
|
|
26
43
|
|
|
44
|
+
/** 插件事件名 → 插件名(前缀补 dsh-,如 task-reliability → dsh-task-reliability)。 */
|
|
45
|
+
function pluginNameOf(name) {
|
|
46
|
+
const prefix = name.split('/')[0]
|
|
47
|
+
return prefix === '' ? name : `dsh-${prefix}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 插件事件名 → 事件名(去掉前缀,如 task-reliability/intervention → intervention)。 */
|
|
51
|
+
function eventNameOf(name) {
|
|
52
|
+
const parts = name.split('/')
|
|
53
|
+
return parts.length > 1 ? parts.slice(1).join('/') : name
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 插件事件 payload → 参数摘要(除 sessionId/action/reason 外的字段;字符串截断防膨胀)。 */
|
|
57
|
+
function summarizeParams(payload) {
|
|
58
|
+
const params = {}
|
|
59
|
+
for (const [key, value] of Object.entries(payload ?? {})) {
|
|
60
|
+
if (key === 'sessionId' || key === 'action' || key === 'reason') continue
|
|
61
|
+
if (typeof value === 'string') params[key] = truncate(value)
|
|
62
|
+
else if (typeof value === 'number' || typeof value === 'boolean') params[key] = value
|
|
63
|
+
}
|
|
64
|
+
return params
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 插件事件 → plugin_event 审计事件(与现有事件同时间线/同过滤/同导出)。
|
|
69
|
+
* 无 sessionId 的插件事件不记录(与 agent_status 等一致)。
|
|
70
|
+
*/
|
|
71
|
+
function handlePluginEvent(name, payload, record) {
|
|
72
|
+
const sessionId = payload?.sessionId
|
|
73
|
+
if (typeof sessionId !== 'string' || sessionId === '') return
|
|
74
|
+
record({
|
|
75
|
+
type: 'plugin_event',
|
|
76
|
+
sessionId,
|
|
77
|
+
data: {
|
|
78
|
+
plugin: pluginNameOf(name),
|
|
79
|
+
event: eventNameOf(name),
|
|
80
|
+
action: truncate(String(payload?.action ?? '')),
|
|
81
|
+
reason: truncate(String(payload?.reason ?? '')),
|
|
82
|
+
params: summarizeParams(payload),
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* session/event → user_message 事件(会话标题来源)。
|
|
89
|
+
* 轨迹回放面板需要"对话可读标题"而非 UUID:从每个会话真实用户的首条
|
|
90
|
+
* 消息截断生成(跳过插件注入消息),面板 sessionsOf 取最早一条作为
|
|
91
|
+
* title。不作为独立存储字段,走现有事件通路,重启后自然恢复。
|
|
92
|
+
*/
|
|
93
|
+
function handleSessionEvent(session, event, record) {
|
|
94
|
+
if (event === null || typeof event !== 'object' || event.type !== 'user/message') return
|
|
95
|
+
const message = event.data
|
|
96
|
+
if (isPluginMessage(message)) return
|
|
97
|
+
const sessionId = session?.id
|
|
98
|
+
if (typeof sessionId !== 'string' || sessionId === '') return
|
|
99
|
+
const text = userTextOf(message)
|
|
100
|
+
if (text === '') return
|
|
101
|
+
record({ type: 'user_message', sessionId, data: { text: truncate(text) } })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 是否为插件注入的消息(非真实用户输入,不作为标题)。 */
|
|
105
|
+
function isPluginMessage(message) {
|
|
106
|
+
const source = message?.source
|
|
107
|
+
return source !== null && typeof source === 'object' && source.kind === 'plugin'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 从 user message 提取文本(content 中全部 text block 拼接)。 */
|
|
111
|
+
function userTextOf(message) {
|
|
112
|
+
if (message === null || typeof message !== 'object') return ''
|
|
113
|
+
const content = message.content
|
|
114
|
+
if (!Array.isArray(content)) return ''
|
|
115
|
+
const parts = []
|
|
116
|
+
for (const block of content) {
|
|
117
|
+
if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
|
|
118
|
+
parts.push(block.text)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return parts.join(' ').trim()
|
|
122
|
+
}
|
|
123
|
+
|
|
27
124
|
/** agent/status → agent_status 事件(含顶层/子代理标记)。 */
|
|
28
125
|
function handleStatus(payload, record) {
|
|
29
126
|
const agent = payload?.agent
|
package/lib/client.js
CHANGED
|
@@ -50,10 +50,16 @@ const strings = {
|
|
|
50
50
|
resourceMem: () => (isZh() ? '内存' : 'Memory'),
|
|
51
51
|
gitTitle: () => (isZh() ? 'Git 工具' : 'Git Tools'),
|
|
52
52
|
allSessions: () => (isZh() ? '全部会话' : 'All sessions'),
|
|
53
|
+
// ── 会话下拉可读性(issue #1xx:只能看到 UUID)────────────────────────
|
|
54
|
+
eventCount: (n) => (isZh() ? `${n} 事件` : `${n} events`),
|
|
55
|
+
sessionFallback: (shortId) => (isZh() ? `会话 ${shortId}` : `session ${shortId}`),
|
|
53
56
|
filterAll: () => (isZh() ? '全部' : 'All'),
|
|
54
57
|
filterStatus: () => (isZh() ? '状态' : 'Status'),
|
|
55
58
|
filterLlm: () => (isZh() ? '模型流' : 'LLM'),
|
|
56
59
|
filterTools: () => (isZh() ? '工具' : 'Tools'),
|
|
60
|
+
filterPlugin: () => (isZh() ? '插件' : 'Plugins'),
|
|
61
|
+
typePluginEvent: () => (isZh() ? '插件事件' : 'plugin event'),
|
|
62
|
+
detailReason: () => (isZh() ? '原因' : 'reason'),
|
|
57
63
|
emptyEvents: () => (isZh() ? '暂无审计事件' : 'No audit events yet'),
|
|
58
64
|
emptyEventsHint: () =>
|
|
59
65
|
isZh()
|
|
@@ -531,12 +537,28 @@ function toolResultParts(data) {
|
|
|
531
537
|
return parts
|
|
532
538
|
}
|
|
533
539
|
|
|
540
|
+
/** 插件事件(issue #154)的搜索片段(插件名/事件名/动作/原因/参数值)。 */
|
|
541
|
+
function pluginEventParts(data) {
|
|
542
|
+
const parts = []
|
|
543
|
+
if (typeof data.plugin === 'string') parts.push(data.plugin)
|
|
544
|
+
if (typeof data.event === 'string') parts.push(data.event)
|
|
545
|
+
if (typeof data.action === 'string') parts.push(data.action)
|
|
546
|
+
if (typeof data.reason === 'string') parts.push(data.reason)
|
|
547
|
+
if (data.params !== null && typeof data.params === 'object') {
|
|
548
|
+
for (const value of Object.values(data.params)) {
|
|
549
|
+
if (typeof value === 'string' && value !== '') parts.push(value)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return parts
|
|
553
|
+
}
|
|
554
|
+
|
|
534
555
|
/** 事件类型 → 搜索片段收集函数(查表消分支)。 */
|
|
535
556
|
const PARTS_COLLECTORS = {
|
|
536
557
|
agent_status: agentStatusParts,
|
|
537
558
|
llm_stream: llmParts,
|
|
538
559
|
tool_call: toolCallParts,
|
|
539
560
|
tool_result: toolResultParts,
|
|
561
|
+
plugin_event: pluginEventParts,
|
|
540
562
|
}
|
|
541
563
|
|
|
542
564
|
/** 提取事件可用于关键词匹配的文本(工具名/参数摘要/错误信息/状态/阶段等)。 */
|
|
@@ -575,10 +597,11 @@ function normalizeCriteria(criteria) {
|
|
|
575
597
|
return { type: criteria.type ?? '', keyword: criteria.keyword ?? '', result: criteria.result ?? '', start, end }
|
|
576
598
|
}
|
|
577
599
|
|
|
578
|
-
/** 类型过滤('tool' 表示 tool_call + tool_result)。 */
|
|
600
|
+
/** 类型过滤('tool' 表示 tool_call + tool_result;'plugin' 表示 plugin_event)。 */
|
|
579
601
|
function passType(type, filterType) {
|
|
580
602
|
if (filterType === '') return true
|
|
581
603
|
if (filterType === 'tool') return type === 'tool_call' || type === 'tool_result'
|
|
604
|
+
if (filterType === 'plugin') return type === 'plugin_event'
|
|
582
605
|
return type === filterType
|
|
583
606
|
}
|
|
584
607
|
|
|
@@ -735,17 +758,21 @@ function typeLabel(event) {
|
|
|
735
758
|
return strings.typeToolCall()
|
|
736
759
|
case 'tool_result':
|
|
737
760
|
return strings.typeToolResult()
|
|
761
|
+
case 'plugin_event':
|
|
762
|
+
return strings.typePluginEvent()
|
|
738
763
|
default:
|
|
739
764
|
return event.type
|
|
740
765
|
}
|
|
741
766
|
}
|
|
742
767
|
|
|
743
768
|
/** 事件类型 → 视觉类别(徽标/图标/节点共用,颜色语义一致):
|
|
744
|
-
* status=info / llm=warn / call=accent / result=success / fail=danger
|
|
769
|
+
* status=info / llm=warn / call=accent / result=success / fail=danger /
|
|
770
|
+
* plugin=info(插件事件复用 info 色,图标区分)。 */
|
|
745
771
|
function typeKind(event) {
|
|
746
772
|
if (event.type === 'agent_status') return 'status'
|
|
747
773
|
if (event.type === 'llm_stream') return 'llm'
|
|
748
774
|
if (event.type === 'tool_call') return 'call'
|
|
775
|
+
if (event.type === 'plugin_event') return 'plugin'
|
|
749
776
|
return event.data?.ok === false ? 'fail' : 'result'
|
|
750
777
|
}
|
|
751
778
|
|
|
@@ -755,6 +782,7 @@ function typeIcon(event) {
|
|
|
755
782
|
if (kind === 'status') return icon.clock(15)
|
|
756
783
|
if (kind === 'llm') return icon.file(15)
|
|
757
784
|
if (kind === 'call') return icon.external(15)
|
|
785
|
+
if (kind === 'plugin') return icon.alert(15)
|
|
758
786
|
if (kind === 'fail') return icon.close(15)
|
|
759
787
|
return icon.check(15)
|
|
760
788
|
}
|
|
@@ -809,6 +837,12 @@ function toolResultMeta(data) {
|
|
|
809
837
|
return `${data.name} · ${result} · ${data.ms}ms`
|
|
810
838
|
}
|
|
811
839
|
|
|
840
|
+
/** 插件事件摘要(插件名 · 事件名 · 动作;issue #154)。 */
|
|
841
|
+
function pluginEventMeta(data) {
|
|
842
|
+
const action = typeof data.action === 'string' && data.action !== '' ? data.action : data.event
|
|
843
|
+
return `${data.plugin} · ${data.event} · ${action}`
|
|
844
|
+
}
|
|
845
|
+
|
|
812
846
|
/** 事件 → 摘要文本(单行,尽力而为)。 */
|
|
813
847
|
function eventMeta(event) {
|
|
814
848
|
const data = event.data || {}
|
|
@@ -816,17 +850,55 @@ function eventMeta(event) {
|
|
|
816
850
|
if (event.type === 'llm_stream') return llmMeta(data)
|
|
817
851
|
if (event.type === 'tool_call') return toolCallMeta(data)
|
|
818
852
|
if (event.type === 'tool_result') return toolResultMeta(data)
|
|
853
|
+
if (event.type === 'plugin_event') return pluginEventMeta(data)
|
|
819
854
|
return ''
|
|
820
855
|
}
|
|
821
856
|
|
|
857
|
+
/** 插件事件详情行(原因 + 参数键值对;非插件事件返回 null 不可展开)。 */
|
|
858
|
+
function eventDetail(event) {
|
|
859
|
+
if (event?.type !== 'plugin_event') return null
|
|
860
|
+
const data = event.data || {}
|
|
861
|
+
const rows = []
|
|
862
|
+
if (typeof data.reason === 'string' && data.reason !== '') {
|
|
863
|
+
rows.push(
|
|
864
|
+
createElement(
|
|
865
|
+
'div',
|
|
866
|
+
{ key: 'reason', className: 'dsh-my-observability-detail-row' },
|
|
867
|
+
createElement('span', { className: 'dsh-my-observability-detail-key' }, strings.detailReason()),
|
|
868
|
+
createElement('span', { className: 'dsh-my-observability-detail-value' }, data.reason),
|
|
869
|
+
),
|
|
870
|
+
)
|
|
871
|
+
}
|
|
872
|
+
if (data.params !== null && typeof data.params === 'object') {
|
|
873
|
+
for (const [key, value] of Object.entries(data.params)) {
|
|
874
|
+
rows.push(
|
|
875
|
+
createElement(
|
|
876
|
+
'div',
|
|
877
|
+
{ key, className: 'dsh-my-observability-detail-row' },
|
|
878
|
+
createElement('span', { className: 'dsh-my-observability-detail-key' }, key),
|
|
879
|
+
createElement('span', { className: 'dsh-my-observability-detail-value' }, String(value)),
|
|
880
|
+
),
|
|
881
|
+
)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return rows.length > 0 ? rows : null
|
|
885
|
+
}
|
|
886
|
+
|
|
822
887
|
/** 单条事件行:节点圆点 + 类型图标 + 徽标/时间 + 摘要(hover/active 反馈)。
|
|
823
|
-
* 摘要命中关键词时以 mark
|
|
888
|
+
* 摘要命中关键词时以 mark 高亮;插件事件可点击展开详情(原因/参数)。 */
|
|
824
889
|
function EventRow({ event, keyword }) {
|
|
825
890
|
const meta = eventMeta(event)
|
|
826
891
|
const kind = typeKind(event)
|
|
892
|
+
const detail = eventDetail(event)
|
|
893
|
+
const [open, setOpen] = useState(false)
|
|
827
894
|
return createElement(
|
|
828
895
|
'button',
|
|
829
|
-
{
|
|
896
|
+
{
|
|
897
|
+
className: 'dsh-my-observability-event',
|
|
898
|
+
type: 'button',
|
|
899
|
+
'aria-expanded': detail !== null ? open : undefined,
|
|
900
|
+
onClick: detail !== null ? () => setOpen(!open) : undefined,
|
|
901
|
+
},
|
|
830
902
|
createElement('span', { className: `dsh-my-observability-node dsh-my-observability-node-${kind}` }),
|
|
831
903
|
createElement(
|
|
832
904
|
'span',
|
|
@@ -853,17 +925,19 @@ function EventRow({ event, keyword }) {
|
|
|
853
925
|
createElement(HighlightText, { text: meta, keyword }),
|
|
854
926
|
)
|
|
855
927
|
: null,
|
|
928
|
+
detail !== null && open ? createElement('div', { className: 'dsh-my-observability-event-detail' }, detail) : null,
|
|
856
929
|
),
|
|
857
930
|
)
|
|
858
931
|
}
|
|
859
932
|
|
|
860
|
-
/** 类型过滤按钮组(aria-pressed
|
|
933
|
+
/** 类型过滤按钮组(aria-pressed 选中态;plugin 过滤插件事件,issue #154)。 */
|
|
861
934
|
function TypeFilter({ filter, onFilter }) {
|
|
862
935
|
const options = [
|
|
863
936
|
['', strings.filterAll()],
|
|
864
937
|
['agent_status', strings.filterStatus()],
|
|
865
938
|
['llm_stream', strings.filterLlm()],
|
|
866
939
|
['tool', strings.filterTools()],
|
|
940
|
+
['plugin', strings.filterPlugin()],
|
|
867
941
|
]
|
|
868
942
|
return createElement(
|
|
869
943
|
'div',
|
|
@@ -907,6 +981,18 @@ async function loadReplayData(selected, currentSession, setters) {
|
|
|
907
981
|
}
|
|
908
982
|
}
|
|
909
983
|
|
|
984
|
+
/** 下拉选项文案:可读标题(首条用户消息)优先,无标题回退 UUID 短显;
|
|
985
|
+
* 附加事件数与时间,用户一眼看出"哪个对话"。 */
|
|
986
|
+
function sessionOptionLabel(s) {
|
|
987
|
+
const title = typeof s.title === 'string' && s.title !== '' ? s.title : strings.sessionFallback(shortId(s.sessionId))
|
|
988
|
+
return `${title} · ${strings.eventCount(s.count)}`
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** 会话 id 短显示(UUID 取前 8 位)。 */
|
|
992
|
+
function shortId(sessionId) {
|
|
993
|
+
return typeof sessionId === 'string' && sessionId.length > 8 ? `${sessionId.slice(0, 8)}…` : sessionId || ''
|
|
994
|
+
}
|
|
995
|
+
|
|
910
996
|
/** 工具栏:会话选择 + 手动刷新 + 类型过滤。 */
|
|
911
997
|
function ReplayToolbar({ sessions, selected, onSelect, filter, onFilter, onRefresh }) {
|
|
912
998
|
return createElement(
|
|
@@ -925,7 +1011,9 @@ function ReplayToolbar({ sessions, selected, onSelect, filter, onFilter, onRefre
|
|
|
925
1011
|
},
|
|
926
1012
|
sessions.length === 0
|
|
927
1013
|
? createElement('option', { value: '' }, strings.allSessions())
|
|
928
|
-
: sessions.map((s) =>
|
|
1014
|
+
: sessions.map((s) =>
|
|
1015
|
+
createElement('option', { key: s.sessionId, value: s.sessionId }, sessionOptionLabel(s)),
|
|
1016
|
+
),
|
|
929
1017
|
),
|
|
930
1018
|
createElement(
|
|
931
1019
|
'button',
|
|
@@ -1050,7 +1138,10 @@ function useResourceState(visible) {
|
|
|
1050
1138
|
if (!visible) return undefined
|
|
1051
1139
|
let alive = true
|
|
1052
1140
|
const tick = () => {
|
|
1053
|
-
if (alive)
|
|
1141
|
+
if (alive)
|
|
1142
|
+
apiJson('/observability/api/resources')
|
|
1143
|
+
.then(setResource)
|
|
1144
|
+
.catch(() => {})
|
|
1054
1145
|
}
|
|
1055
1146
|
tick()
|
|
1056
1147
|
const timer = setInterval(tick, RESOURCE_POLL_MS)
|
|
@@ -1085,8 +1176,14 @@ function ResourcePanel({ resource }) {
|
|
|
1085
1176
|
'div',
|
|
1086
1177
|
{ className: 'dsh-my-observability-resource-grid' },
|
|
1087
1178
|
createElement(ResourceMetric, { label: strings.resourceFile(), value: fmtResourceBytes(resource.fileBytes) }),
|
|
1088
|
-
createElement(ResourceMetric, {
|
|
1089
|
-
|
|
1179
|
+
createElement(ResourceMetric, {
|
|
1180
|
+
label: strings.resourceRate(),
|
|
1181
|
+
value: `${fmtResourceBytes(resource.writeRateBytesPerHour)}/h`,
|
|
1182
|
+
}),
|
|
1183
|
+
createElement(ResourceMetric, {
|
|
1184
|
+
label: strings.resourceCpu(),
|
|
1185
|
+
value: `${Math.round(resource.cpuPercent ?? 0)}%`,
|
|
1186
|
+
}),
|
|
1090
1187
|
createElement(ResourceMetric, { label: strings.resourceMem(), value: fmtResourceBytes(resource.memoryBytes) }),
|
|
1091
1188
|
),
|
|
1092
1189
|
alerts.length > 0
|
|
@@ -1386,7 +1483,10 @@ function useReplayDataState(props) {
|
|
|
1386
1483
|
if (!visible) return undefined
|
|
1387
1484
|
let alive = true
|
|
1388
1485
|
const tick = () => {
|
|
1389
|
-
if (alive)
|
|
1486
|
+
if (alive)
|
|
1487
|
+
apiJson('/observability/api/resources')
|
|
1488
|
+
.then(setResource)
|
|
1489
|
+
.catch(() => {})
|
|
1390
1490
|
}
|
|
1391
1491
|
tick()
|
|
1392
1492
|
const timer = setInterval(tick, RESOURCE_POLL_MS)
|
|
@@ -1842,12 +1942,14 @@ const STYLES = `
|
|
|
1842
1942
|
.dsh-my-observability-node-status{border-color:var(--dsw-alias-state-info-primary)}
|
|
1843
1943
|
.dsh-my-observability-node-llm{border-color:var(--dsw-alias-state-warn-primary)}
|
|
1844
1944
|
.dsh-my-observability-node-call{border-color:var(--dsw-alias-accent)}
|
|
1945
|
+
.dsh-my-observability-node-plugin{border-color:var(--dsw-alias-state-info-primary)}
|
|
1845
1946
|
.dsh-my-observability-node-result{border-color:var(--dsw-alias-state-success-primary)}
|
|
1846
1947
|
.dsh-my-observability-node-fail{border-color:var(--dsw-alias-state-error-primary)}
|
|
1847
1948
|
.dsh-my-observability-event-icon{flex:none;display:flex;align-items:center;margin-top:1px}
|
|
1848
1949
|
.dsh-my-observability-icon-status{color:var(--dsw-alias-state-info-primary)}
|
|
1849
1950
|
.dsh-my-observability-icon-llm{color:var(--dsw-alias-state-warn-primary)}
|
|
1850
1951
|
.dsh-my-observability-icon-call{color:var(--dsw-alias-accent)}
|
|
1952
|
+
.dsh-my-observability-icon-plugin{color:var(--dsw-alias-state-info-primary)}
|
|
1851
1953
|
.dsh-my-observability-icon-result{color:var(--dsw-alias-state-success-primary)}
|
|
1852
1954
|
.dsh-my-observability-icon-fail{color:var(--dsw-alias-state-error-primary)}
|
|
1853
1955
|
.dsh-my-observability-event-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
|
|
@@ -1856,10 +1958,17 @@ const STYLES = `
|
|
|
1856
1958
|
.dsh-my-observability-badge-status{color:var(--dsw-alias-state-info-primary);background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent)}
|
|
1857
1959
|
.dsh-my-observability-badge-llm{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent)}
|
|
1858
1960
|
.dsh-my-observability-badge-call{color:var(--dsw-alias-accent);background:color-mix(in srgb, var(--dsw-alias-accent) 12%, transparent)}
|
|
1961
|
+
.dsh-my-observability-badge-plugin{color:var(--dsw-alias-state-info-primary);background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent)}
|
|
1859
1962
|
.dsh-my-observability-badge-result{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)}
|
|
1860
1963
|
.dsh-my-observability-badge-fail{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent)}
|
|
1861
1964
|
.dsh-my-observability-time{flex:none;font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-tertiary);white-space:nowrap}
|
|
1862
1965
|
.dsh-my-observability-event-meta{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);line-height:1.6;word-break:break-word}
|
|
1966
|
+
/* ── 插件事件详情展开(issue #154:原因/参数可查)── */
|
|
1967
|
+
.dsh-my-observability-event-detail{display:flex;flex-direction:column;gap:2px;margin-top:4px;padding:6px 8px;
|
|
1968
|
+
border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1)}
|
|
1969
|
+
.dsh-my-observability-detail-row{display:flex;gap:8px;font:var(--dsw-font-xxs-12);line-height:1.5;word-break:break-word}
|
|
1970
|
+
.dsh-my-observability-detail-key{flex:none;font:var(--dsw-font-xxxs-strong-11);color:var(--dsw-alias-label-tertiary);min-width:56px}
|
|
1971
|
+
.dsh-my-observability-detail-value{color:var(--dsw-alias-label-primary)}
|
|
1863
1972
|
/* ── 状态区:loading / 空 / 错误 ── */
|
|
1864
1973
|
.dsh-my-observability-state{display:flex;align-items:center;gap:6px;padding:8px 6px;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-tertiary)}
|
|
1865
1974
|
.dsh-my-observability-state svg{flex:none;animation:dsh-my-observability-spin 1s linear infinite}
|
package/lib/parts/i18n.js
CHANGED
|
@@ -18,10 +18,16 @@ const strings = {
|
|
|
18
18
|
resourceMem: () => (isZh() ? '内存' : 'Memory'),
|
|
19
19
|
gitTitle: () => (isZh() ? 'Git 工具' : 'Git Tools'),
|
|
20
20
|
allSessions: () => (isZh() ? '全部会话' : 'All sessions'),
|
|
21
|
+
// ── 会话下拉可读性(issue #1xx:只能看到 UUID)────────────────────────
|
|
22
|
+
eventCount: (n) => (isZh() ? `${n} 事件` : `${n} events`),
|
|
23
|
+
sessionFallback: (shortId) => (isZh() ? `会话 ${shortId}` : `session ${shortId}`),
|
|
21
24
|
filterAll: () => (isZh() ? '全部' : 'All'),
|
|
22
25
|
filterStatus: () => (isZh() ? '状态' : 'Status'),
|
|
23
26
|
filterLlm: () => (isZh() ? '模型流' : 'LLM'),
|
|
24
27
|
filterTools: () => (isZh() ? '工具' : 'Tools'),
|
|
28
|
+
filterPlugin: () => (isZh() ? '插件' : 'Plugins'),
|
|
29
|
+
typePluginEvent: () => (isZh() ? '插件事件' : 'plugin event'),
|
|
30
|
+
detailReason: () => (isZh() ? '原因' : 'reason'),
|
|
25
31
|
emptyEvents: () => (isZh() ? '暂无审计事件' : 'No audit events yet'),
|
|
26
32
|
emptyEventsHint: () =>
|
|
27
33
|
isZh()
|
package/lib/parts/replay.js
CHANGED
|
@@ -22,17 +22,21 @@ function typeLabel(event) {
|
|
|
22
22
|
return strings.typeToolCall()
|
|
23
23
|
case 'tool_result':
|
|
24
24
|
return strings.typeToolResult()
|
|
25
|
+
case 'plugin_event':
|
|
26
|
+
return strings.typePluginEvent()
|
|
25
27
|
default:
|
|
26
28
|
return event.type
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** 事件类型 → 视觉类别(徽标/图标/节点共用,颜色语义一致):
|
|
31
|
-
* status=info / llm=warn / call=accent / result=success / fail=danger
|
|
33
|
+
* status=info / llm=warn / call=accent / result=success / fail=danger /
|
|
34
|
+
* plugin=info(插件事件复用 info 色,图标区分)。 */
|
|
32
35
|
function typeKind(event) {
|
|
33
36
|
if (event.type === 'agent_status') return 'status'
|
|
34
37
|
if (event.type === 'llm_stream') return 'llm'
|
|
35
38
|
if (event.type === 'tool_call') return 'call'
|
|
39
|
+
if (event.type === 'plugin_event') return 'plugin'
|
|
36
40
|
return event.data?.ok === false ? 'fail' : 'result'
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -42,6 +46,7 @@ function typeIcon(event) {
|
|
|
42
46
|
if (kind === 'status') return icon.clock(15)
|
|
43
47
|
if (kind === 'llm') return icon.file(15)
|
|
44
48
|
if (kind === 'call') return icon.external(15)
|
|
49
|
+
if (kind === 'plugin') return icon.alert(15)
|
|
45
50
|
if (kind === 'fail') return icon.close(15)
|
|
46
51
|
return icon.check(15)
|
|
47
52
|
}
|
|
@@ -96,6 +101,12 @@ function toolResultMeta(data) {
|
|
|
96
101
|
return `${data.name} · ${result} · ${data.ms}ms`
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
/** 插件事件摘要(插件名 · 事件名 · 动作;issue #154)。 */
|
|
105
|
+
function pluginEventMeta(data) {
|
|
106
|
+
const action = typeof data.action === 'string' && data.action !== '' ? data.action : data.event
|
|
107
|
+
return `${data.plugin} · ${data.event} · ${action}`
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
/** 事件 → 摘要文本(单行,尽力而为)。 */
|
|
100
111
|
function eventMeta(event) {
|
|
101
112
|
const data = event.data || {}
|
|
@@ -103,17 +114,55 @@ function eventMeta(event) {
|
|
|
103
114
|
if (event.type === 'llm_stream') return llmMeta(data)
|
|
104
115
|
if (event.type === 'tool_call') return toolCallMeta(data)
|
|
105
116
|
if (event.type === 'tool_result') return toolResultMeta(data)
|
|
117
|
+
if (event.type === 'plugin_event') return pluginEventMeta(data)
|
|
106
118
|
return ''
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
/** 插件事件详情行(原因 + 参数键值对;非插件事件返回 null 不可展开)。 */
|
|
122
|
+
function eventDetail(event) {
|
|
123
|
+
if (event?.type !== 'plugin_event') return null
|
|
124
|
+
const data = event.data || {}
|
|
125
|
+
const rows = []
|
|
126
|
+
if (typeof data.reason === 'string' && data.reason !== '') {
|
|
127
|
+
rows.push(
|
|
128
|
+
createElement(
|
|
129
|
+
'div',
|
|
130
|
+
{ key: 'reason', className: 'dsh-my-observability-detail-row' },
|
|
131
|
+
createElement('span', { className: 'dsh-my-observability-detail-key' }, strings.detailReason()),
|
|
132
|
+
createElement('span', { className: 'dsh-my-observability-detail-value' }, data.reason),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
if (data.params !== null && typeof data.params === 'object') {
|
|
137
|
+
for (const [key, value] of Object.entries(data.params)) {
|
|
138
|
+
rows.push(
|
|
139
|
+
createElement(
|
|
140
|
+
'div',
|
|
141
|
+
{ key, className: 'dsh-my-observability-detail-row' },
|
|
142
|
+
createElement('span', { className: 'dsh-my-observability-detail-key' }, key),
|
|
143
|
+
createElement('span', { className: 'dsh-my-observability-detail-value' }, String(value)),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return rows.length > 0 ? rows : null
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
/** 单条事件行:节点圆点 + 类型图标 + 徽标/时间 + 摘要(hover/active 反馈)。
|
|
110
|
-
* 摘要命中关键词时以 mark
|
|
152
|
+
* 摘要命中关键词时以 mark 高亮;插件事件可点击展开详情(原因/参数)。 */
|
|
111
153
|
function EventRow({ event, keyword }) {
|
|
112
154
|
const meta = eventMeta(event)
|
|
113
155
|
const kind = typeKind(event)
|
|
156
|
+
const detail = eventDetail(event)
|
|
157
|
+
const [open, setOpen] = useState(false)
|
|
114
158
|
return createElement(
|
|
115
159
|
'button',
|
|
116
|
-
{
|
|
160
|
+
{
|
|
161
|
+
className: 'dsh-my-observability-event',
|
|
162
|
+
type: 'button',
|
|
163
|
+
'aria-expanded': detail !== null ? open : undefined,
|
|
164
|
+
onClick: detail !== null ? () => setOpen(!open) : undefined,
|
|
165
|
+
},
|
|
117
166
|
createElement('span', { className: `dsh-my-observability-node dsh-my-observability-node-${kind}` }),
|
|
118
167
|
createElement(
|
|
119
168
|
'span',
|
|
@@ -140,17 +189,19 @@ function EventRow({ event, keyword }) {
|
|
|
140
189
|
createElement(HighlightText, { text: meta, keyword }),
|
|
141
190
|
)
|
|
142
191
|
: null,
|
|
192
|
+
detail !== null && open ? createElement('div', { className: 'dsh-my-observability-event-detail' }, detail) : null,
|
|
143
193
|
),
|
|
144
194
|
)
|
|
145
195
|
}
|
|
146
196
|
|
|
147
|
-
/** 类型过滤按钮组(aria-pressed
|
|
197
|
+
/** 类型过滤按钮组(aria-pressed 选中态;plugin 过滤插件事件,issue #154)。 */
|
|
148
198
|
function TypeFilter({ filter, onFilter }) {
|
|
149
199
|
const options = [
|
|
150
200
|
['', strings.filterAll()],
|
|
151
201
|
['agent_status', strings.filterStatus()],
|
|
152
202
|
['llm_stream', strings.filterLlm()],
|
|
153
203
|
['tool', strings.filterTools()],
|
|
204
|
+
['plugin', strings.filterPlugin()],
|
|
154
205
|
]
|
|
155
206
|
return createElement(
|
|
156
207
|
'div',
|
|
@@ -194,6 +245,18 @@ async function loadReplayData(selected, currentSession, setters) {
|
|
|
194
245
|
}
|
|
195
246
|
}
|
|
196
247
|
|
|
248
|
+
/** 下拉选项文案:可读标题(首条用户消息)优先,无标题回退 UUID 短显;
|
|
249
|
+
* 附加事件数与时间,用户一眼看出"哪个对话"。 */
|
|
250
|
+
function sessionOptionLabel(s) {
|
|
251
|
+
const title = typeof s.title === 'string' && s.title !== '' ? s.title : strings.sessionFallback(shortId(s.sessionId))
|
|
252
|
+
return `${title} · ${strings.eventCount(s.count)}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 会话 id 短显示(UUID 取前 8 位)。 */
|
|
256
|
+
function shortId(sessionId) {
|
|
257
|
+
return typeof sessionId === 'string' && sessionId.length > 8 ? `${sessionId.slice(0, 8)}…` : sessionId || ''
|
|
258
|
+
}
|
|
259
|
+
|
|
197
260
|
/** 工具栏:会话选择 + 手动刷新 + 类型过滤。 */
|
|
198
261
|
function ReplayToolbar({ sessions, selected, onSelect, filter, onFilter, onRefresh }) {
|
|
199
262
|
return createElement(
|
|
@@ -212,7 +275,9 @@ function ReplayToolbar({ sessions, selected, onSelect, filter, onFilter, onRefre
|
|
|
212
275
|
},
|
|
213
276
|
sessions.length === 0
|
|
214
277
|
? createElement('option', { value: '' }, strings.allSessions())
|
|
215
|
-
: sessions.map((s) =>
|
|
278
|
+
: sessions.map((s) =>
|
|
279
|
+
createElement('option', { key: s.sessionId, value: s.sessionId }, sessionOptionLabel(s)),
|
|
280
|
+
),
|
|
216
281
|
),
|
|
217
282
|
createElement(
|
|
218
283
|
'button',
|
package/lib/parts/styles.js
CHANGED
|
@@ -42,12 +42,14 @@ const STYLES = `
|
|
|
42
42
|
.dsh-my-observability-node-status{border-color:var(--dsw-alias-state-info-primary)}
|
|
43
43
|
.dsh-my-observability-node-llm{border-color:var(--dsw-alias-state-warn-primary)}
|
|
44
44
|
.dsh-my-observability-node-call{border-color:var(--dsw-alias-accent)}
|
|
45
|
+
.dsh-my-observability-node-plugin{border-color:var(--dsw-alias-state-info-primary)}
|
|
45
46
|
.dsh-my-observability-node-result{border-color:var(--dsw-alias-state-success-primary)}
|
|
46
47
|
.dsh-my-observability-node-fail{border-color:var(--dsw-alias-state-error-primary)}
|
|
47
48
|
.dsh-my-observability-event-icon{flex:none;display:flex;align-items:center;margin-top:1px}
|
|
48
49
|
.dsh-my-observability-icon-status{color:var(--dsw-alias-state-info-primary)}
|
|
49
50
|
.dsh-my-observability-icon-llm{color:var(--dsw-alias-state-warn-primary)}
|
|
50
51
|
.dsh-my-observability-icon-call{color:var(--dsw-alias-accent)}
|
|
52
|
+
.dsh-my-observability-icon-plugin{color:var(--dsw-alias-state-info-primary)}
|
|
51
53
|
.dsh-my-observability-icon-result{color:var(--dsw-alias-state-success-primary)}
|
|
52
54
|
.dsh-my-observability-icon-fail{color:var(--dsw-alias-state-error-primary)}
|
|
53
55
|
.dsh-my-observability-event-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
|
|
@@ -56,10 +58,17 @@ const STYLES = `
|
|
|
56
58
|
.dsh-my-observability-badge-status{color:var(--dsw-alias-state-info-primary);background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent)}
|
|
57
59
|
.dsh-my-observability-badge-llm{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent)}
|
|
58
60
|
.dsh-my-observability-badge-call{color:var(--dsw-alias-accent);background:color-mix(in srgb, var(--dsw-alias-accent) 12%, transparent)}
|
|
61
|
+
.dsh-my-observability-badge-plugin{color:var(--dsw-alias-state-info-primary);background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent)}
|
|
59
62
|
.dsh-my-observability-badge-result{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)}
|
|
60
63
|
.dsh-my-observability-badge-fail{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent)}
|
|
61
64
|
.dsh-my-observability-time{flex:none;font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-tertiary);white-space:nowrap}
|
|
62
65
|
.dsh-my-observability-event-meta{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);line-height:1.6;word-break:break-word}
|
|
66
|
+
/* ── 插件事件详情展开(issue #154:原因/参数可查)── */
|
|
67
|
+
.dsh-my-observability-event-detail{display:flex;flex-direction:column;gap:2px;margin-top:4px;padding:6px 8px;
|
|
68
|
+
border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1)}
|
|
69
|
+
.dsh-my-observability-detail-row{display:flex;gap:8px;font:var(--dsw-font-xxs-12);line-height:1.5;word-break:break-word}
|
|
70
|
+
.dsh-my-observability-detail-key{flex:none;font:var(--dsw-font-xxxs-strong-11);color:var(--dsw-alias-label-tertiary);min-width:56px}
|
|
71
|
+
.dsh-my-observability-detail-value{color:var(--dsw-alias-label-primary)}
|
|
63
72
|
/* ── 状态区:loading / 空 / 错误 ── */
|
|
64
73
|
.dsh-my-observability-state{display:flex;align-items:center;gap:6px;padding:8px 6px;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-tertiary)}
|
|
65
74
|
.dsh-my-observability-state svg{flex:none;animation:dsh-my-observability-spin 1s linear infinite}
|
package/lib/store.js
CHANGED
|
@@ -134,7 +134,7 @@ function eventsOf(handle, sessionId, type, limit) {
|
|
|
134
134
|
return capped.map((event) => ({ ...event }))
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
137
|
+
/** 有审计事件的会话列表(按最后活动时间倒序,含事件数与可读标题)。 */
|
|
138
138
|
function sessionsOf(handle) {
|
|
139
139
|
const entries = Object.entries(handle.store.state.bySession)
|
|
140
140
|
const list = entries
|
|
@@ -142,12 +142,23 @@ function sessionsOf(handle) {
|
|
|
142
142
|
sessionId,
|
|
143
143
|
count: bucket.events.length,
|
|
144
144
|
lastTime: bucket.events.length > 0 ? bucket.events[bucket.events.length - 1].time : 0,
|
|
145
|
+
title: sessionTitleOf(bucket),
|
|
145
146
|
}))
|
|
146
147
|
.filter((entry) => entry.count > 0)
|
|
147
148
|
list.sort((a, b) => b.lastTime - a.lastTime)
|
|
148
149
|
return list
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/** 会话标题:最早一条 user_message 事件的文本(无则空串,面板回退 UUID 短显)。 */
|
|
153
|
+
function sessionTitleOf(bucket) {
|
|
154
|
+
for (const event of bucket.events) {
|
|
155
|
+
if (event.type === 'user_message' && typeof event.data?.text === 'string' && event.data.text !== '') {
|
|
156
|
+
return event.data.text
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return ''
|
|
160
|
+
}
|
|
161
|
+
|
|
151
162
|
/** 全部会话事件总数(O(1) 计数)。 */
|
|
152
163
|
function countOf(handle) {
|
|
153
164
|
return handle.total
|
|
@@ -270,7 +281,9 @@ function compactNow(handle) {
|
|
|
270
281
|
})
|
|
271
282
|
}
|
|
272
283
|
|
|
273
|
-
/** 卸载冲刷:清定时器 + 回放未就绪缓冲 + 立即落盘(迁移兜底)。
|
|
284
|
+
/** 卸载冲刷:清定时器 + 回放未就绪缓冲 + 立即落盘(迁移兜底)。
|
|
285
|
+
* 返回落盘链 promise:调用方 await 可保证冲刷完成(卸载/退出时事件
|
|
286
|
+
* 不丢);fire-and-forget 调用亦兼容(触发后异步落盘)。 */
|
|
274
287
|
function dispose(handle) {
|
|
275
288
|
if (handle.flushTimer !== null) {
|
|
276
289
|
clearTimeout(handle.flushTimer)
|
|
@@ -286,5 +299,5 @@ function dispose(handle) {
|
|
|
286
299
|
}
|
|
287
300
|
flushNow(handle)
|
|
288
301
|
if (handle.migrated || handle.queuedLines >= COMPACT_LINES) compactNow(handle)
|
|
289
|
-
|
|
302
|
+
return handle.dirtyChain
|
|
290
303
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-my-observability",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "DSH 可观测性 + Git 工程工具插件:会话轨迹回放(时间轴)、事件审计(agent 行为记录)、结构化 Git 提交(Conventional Commits)、增量 diff 审查(提交前审查)。DSH web plugin: trajectory replay timeline, event audit log, structured git commits, pre-commit incremental diff review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|