dsh-my-observability 0.1.6 → 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,14 @@
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
+
5
13
  ## [0.1.6] - 2026-09-04
6
14
 
7
15
  ### 变更
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,6 +16,19 @@
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 [
@@ -22,9 +37,53 @@ export function attachAuditListeners(ctx, record) {
22
37
  ctx.on('llm/stream', (options, next) => handleStream(options, next, record)),
23
38
  ctx.on('tools/pre-execute', (exec, next) => handlePreExecute(exec, next, record)),
24
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))),
25
41
  ]
26
42
  }
27
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
+
28
87
  /**
29
88
  * session/event → user_message 事件(会话标题来源)。
30
89
  * 轨迹回放面板需要"对话可读标题"而非 UUID:从每个会话真实用户的首条
package/lib/client.js CHANGED
@@ -57,6 +57,9 @@ const strings = {
57
57
  filterStatus: () => (isZh() ? '状态' : 'Status'),
58
58
  filterLlm: () => (isZh() ? '模型流' : 'LLM'),
59
59
  filterTools: () => (isZh() ? '工具' : 'Tools'),
60
+ filterPlugin: () => (isZh() ? '插件' : 'Plugins'),
61
+ typePluginEvent: () => (isZh() ? '插件事件' : 'plugin event'),
62
+ detailReason: () => (isZh() ? '原因' : 'reason'),
60
63
  emptyEvents: () => (isZh() ? '暂无审计事件' : 'No audit events yet'),
61
64
  emptyEventsHint: () =>
62
65
  isZh()
@@ -534,12 +537,28 @@ function toolResultParts(data) {
534
537
  return parts
535
538
  }
536
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
+
537
555
  /** 事件类型 → 搜索片段收集函数(查表消分支)。 */
538
556
  const PARTS_COLLECTORS = {
539
557
  agent_status: agentStatusParts,
540
558
  llm_stream: llmParts,
541
559
  tool_call: toolCallParts,
542
560
  tool_result: toolResultParts,
561
+ plugin_event: pluginEventParts,
543
562
  }
544
563
 
545
564
  /** 提取事件可用于关键词匹配的文本(工具名/参数摘要/错误信息/状态/阶段等)。 */
@@ -578,10 +597,11 @@ function normalizeCriteria(criteria) {
578
597
  return { type: criteria.type ?? '', keyword: criteria.keyword ?? '', result: criteria.result ?? '', start, end }
579
598
  }
580
599
 
581
- /** 类型过滤('tool' 表示 tool_call + tool_result)。 */
600
+ /** 类型过滤('tool' 表示 tool_call + tool_result;'plugin' 表示 plugin_event)。 */
582
601
  function passType(type, filterType) {
583
602
  if (filterType === '') return true
584
603
  if (filterType === 'tool') return type === 'tool_call' || type === 'tool_result'
604
+ if (filterType === 'plugin') return type === 'plugin_event'
585
605
  return type === filterType
586
606
  }
587
607
 
@@ -738,17 +758,21 @@ function typeLabel(event) {
738
758
  return strings.typeToolCall()
739
759
  case 'tool_result':
740
760
  return strings.typeToolResult()
761
+ case 'plugin_event':
762
+ return strings.typePluginEvent()
741
763
  default:
742
764
  return event.type
743
765
  }
744
766
  }
745
767
 
746
768
  /** 事件类型 → 视觉类别(徽标/图标/节点共用,颜色语义一致):
747
- * 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 色,图标区分)。 */
748
771
  function typeKind(event) {
749
772
  if (event.type === 'agent_status') return 'status'
750
773
  if (event.type === 'llm_stream') return 'llm'
751
774
  if (event.type === 'tool_call') return 'call'
775
+ if (event.type === 'plugin_event') return 'plugin'
752
776
  return event.data?.ok === false ? 'fail' : 'result'
753
777
  }
754
778
 
@@ -758,6 +782,7 @@ function typeIcon(event) {
758
782
  if (kind === 'status') return icon.clock(15)
759
783
  if (kind === 'llm') return icon.file(15)
760
784
  if (kind === 'call') return icon.external(15)
785
+ if (kind === 'plugin') return icon.alert(15)
761
786
  if (kind === 'fail') return icon.close(15)
762
787
  return icon.check(15)
763
788
  }
@@ -812,6 +837,12 @@ function toolResultMeta(data) {
812
837
  return `${data.name} · ${result} · ${data.ms}ms`
813
838
  }
814
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
+
815
846
  /** 事件 → 摘要文本(单行,尽力而为)。 */
816
847
  function eventMeta(event) {
817
848
  const data = event.data || {}
@@ -819,17 +850,55 @@ function eventMeta(event) {
819
850
  if (event.type === 'llm_stream') return llmMeta(data)
820
851
  if (event.type === 'tool_call') return toolCallMeta(data)
821
852
  if (event.type === 'tool_result') return toolResultMeta(data)
853
+ if (event.type === 'plugin_event') return pluginEventMeta(data)
822
854
  return ''
823
855
  }
824
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
+
825
887
  /** 单条事件行:节点圆点 + 类型图标 + 徽标/时间 + 摘要(hover/active 反馈)。
826
- * 摘要命中关键词时以 mark 高亮。 */
888
+ * 摘要命中关键词时以 mark 高亮;插件事件可点击展开详情(原因/参数)。 */
827
889
  function EventRow({ event, keyword }) {
828
890
  const meta = eventMeta(event)
829
891
  const kind = typeKind(event)
892
+ const detail = eventDetail(event)
893
+ const [open, setOpen] = useState(false)
830
894
  return createElement(
831
895
  'button',
832
- { 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
+ },
833
902
  createElement('span', { className: `dsh-my-observability-node dsh-my-observability-node-${kind}` }),
834
903
  createElement(
835
904
  'span',
@@ -856,17 +925,19 @@ function EventRow({ event, keyword }) {
856
925
  createElement(HighlightText, { text: meta, keyword }),
857
926
  )
858
927
  : null,
928
+ detail !== null && open ? createElement('div', { className: 'dsh-my-observability-event-detail' }, detail) : null,
859
929
  ),
860
930
  )
861
931
  }
862
932
 
863
- /** 类型过滤按钮组(aria-pressed 选中态)。 */
933
+ /** 类型过滤按钮组(aria-pressed 选中态;plugin 过滤插件事件,issue #154)。 */
864
934
  function TypeFilter({ filter, onFilter }) {
865
935
  const options = [
866
936
  ['', strings.filterAll()],
867
937
  ['agent_status', strings.filterStatus()],
868
938
  ['llm_stream', strings.filterLlm()],
869
939
  ['tool', strings.filterTools()],
940
+ ['plugin', strings.filterPlugin()],
870
941
  ]
871
942
  return createElement(
872
943
  'div',
@@ -1871,12 +1942,14 @@ const STYLES = `
1871
1942
  .dsh-my-observability-node-status{border-color:var(--dsw-alias-state-info-primary)}
1872
1943
  .dsh-my-observability-node-llm{border-color:var(--dsw-alias-state-warn-primary)}
1873
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)}
1874
1946
  .dsh-my-observability-node-result{border-color:var(--dsw-alias-state-success-primary)}
1875
1947
  .dsh-my-observability-node-fail{border-color:var(--dsw-alias-state-error-primary)}
1876
1948
  .dsh-my-observability-event-icon{flex:none;display:flex;align-items:center;margin-top:1px}
1877
1949
  .dsh-my-observability-icon-status{color:var(--dsw-alias-state-info-primary)}
1878
1950
  .dsh-my-observability-icon-llm{color:var(--dsw-alias-state-warn-primary)}
1879
1951
  .dsh-my-observability-icon-call{color:var(--dsw-alias-accent)}
1952
+ .dsh-my-observability-icon-plugin{color:var(--dsw-alias-state-info-primary)}
1880
1953
  .dsh-my-observability-icon-result{color:var(--dsw-alias-state-success-primary)}
1881
1954
  .dsh-my-observability-icon-fail{color:var(--dsw-alias-state-error-primary)}
1882
1955
  .dsh-my-observability-event-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
@@ -1885,10 +1958,17 @@ const STYLES = `
1885
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)}
1886
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)}
1887
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)}
1888
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)}
1889
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)}
1890
1964
  .dsh-my-observability-time{flex:none;font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-tertiary);white-space:nowrap}
1891
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)}
1892
1972
  /* ── 状态区:loading / 空 / 错误 ── */
1893
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)}
1894
1974
  .dsh-my-observability-state svg{flex:none;animation:dsh-my-observability-spin 1s linear infinite}
package/lib/parts/i18n.js CHANGED
@@ -25,6 +25,9 @@ const strings = {
25
25
  filterStatus: () => (isZh() ? '状态' : 'Status'),
26
26
  filterLlm: () => (isZh() ? '模型流' : 'LLM'),
27
27
  filterTools: () => (isZh() ? '工具' : 'Tools'),
28
+ filterPlugin: () => (isZh() ? '插件' : 'Plugins'),
29
+ typePluginEvent: () => (isZh() ? '插件事件' : 'plugin event'),
30
+ detailReason: () => (isZh() ? '原因' : 'reason'),
28
31
  emptyEvents: () => (isZh() ? '暂无审计事件' : 'No audit events yet'),
29
32
  emptyEventsHint: () =>
30
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',
@@ -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
@@ -281,7 +281,9 @@ function compactNow(handle) {
281
281
  })
282
282
  }
283
283
 
284
- /** 卸载冲刷:清定时器 + 回放未就绪缓冲 + 立即落盘(迁移兜底)。 */
284
+ /** 卸载冲刷:清定时器 + 回放未就绪缓冲 + 立即落盘(迁移兜底)。
285
+ * 返回落盘链 promise:调用方 await 可保证冲刷完成(卸载/退出时事件
286
+ * 不丢);fire-and-forget 调用亦兼容(触发后异步落盘)。 */
285
287
  function dispose(handle) {
286
288
  if (handle.flushTimer !== null) {
287
289
  clearTimeout(handle.flushTimer)
@@ -297,5 +299,5 @@ function dispose(handle) {
297
299
  }
298
300
  flushNow(handle)
299
301
  if (handle.migrated || handle.queuedLines >= COMPACT_LINES) compactNow(handle)
300
- void handle.dirtyChain
302
+ return handle.dirtyChain
301
303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-my-observability",
3
- "version": "0.1.6",
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",