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 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?.events
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
- { className: 'dsh-my-observability-event', type: 'button' },
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) => createElement('option', { key: s.sessionId, value: s.sessionId }, s.sessionId)),
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) apiJson('/observability/api/resources').then(setResource).catch(() => {})
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, { label: strings.resourceRate(), value: `${fmtResourceBytes(resource.writeRateBytesPerHour)}/h` }),
1089
- createElement(ResourceMetric, { label: strings.resourceCpu(), value: `${Math.round(resource.cpuPercent ?? 0)}%` }),
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) apiJson('/observability/api/resources').then(setResource).catch(() => {})
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()
@@ -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
- { className: 'dsh-my-observability-event', type: 'button' },
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) => createElement('option', { key: s.sessionId, value: s.sessionId }, s.sessionId)),
278
+ : sessions.map((s) =>
279
+ createElement('option', { key: s.sessionId, value: s.sessionId }, sessionOptionLabel(s)),
280
+ ),
216
281
  ),
217
282
  createElement(
218
283
  'button',
@@ -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
- void handle.dirtyChain
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.5",
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",