dsh-my-observability 0.3.0 → 0.3.2

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/lib/client.js CHANGED
@@ -13,10 +13,18 @@
13
13
  * 样式走 DSH 语义 token(--dsw-alias-* / --dsw-font-*),随 activation
14
14
  * 注入、fiber teardown 卸载(HMR/禁用无残留)。
15
15
  *
16
+ * 侧边栏走**宿主原生扩展点**(issue #187 批 1),不再消费第三方侧边栏服务:
17
+ * sidebarRightTabs 注册页签类型(guide 胶囊供用户从右栏指南页打开),slots 的
18
+ * sidebar.right.pane.tab / .title 两个 keyed 席位注册面板与标题(key = 类型 id)。
19
+ * 原生能力经 Cordis 服务名 inject 获取,无需 require 任何
20
+ * @deepseek-ai/dsh-client-ui-* 包(官方范本:
21
+ * dsh-client-ui-sidebar-files/lib/client.js:681-711)。
22
+ *
16
23
  * BUILD NOTE: 本文件是模板源码,不是 DSH 实际服务的文件。scripts/build.mjs
17
- * 将片段文件(lib/parts/i18n.js / replay.js / git.js / styles.js,均为
18
- * 无 import/export 的纯函数声明文本;图标片段来自 dsh-shared 共享
19
- * client-parts,见 docs/UI规范.md)经下方 __PART_*__ 占位符(函数式
24
+ * 先编译 client TS 片段(src/client/parts/*.ts → lib/.client-build/parts/*.js),
25
+ * 再将这些片段(无 import/export 的纯函数声明文本;图标片段来自 dsh-shared
26
+ * 共享 client-parts,见 docs/UI规范.md;audit-view 片段来自 server 端产物
27
+ * lib/audit-view.js,剥离 export 前缀)经下方 __PART_*__ 占位符(函数式
20
28
  * replaceAll,避免 $&/$1 特殊解释)拼接进 factory 作用域,写出
21
29
  * lib/client.js —— 即 DSH 实际服务的产物。产物必须提交;CI 只对产物执行
22
30
  * node --check(见 scripts/test-all.sh / .github/workflows/ci.yml)。
@@ -29,111 +37,164 @@ window.__ModuleLoader__.load({
29
37
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
30
38
  const { createElement, useEffect, useState } = require('react')
31
39
 
40
+ // 原生页签身份(issue #187 批 1):id/kind = 迁移前 better-sidebar 的 tab id
41
+ // (每个面板一个类型,id 全局唯一且是席位的 key),order = 迁移前的指南页顺序。
42
+ const REPLAY_TAB_ID = 'dsh-my-observability:replay'
43
+ const GIT_TAB_ID = 'dsh-my-observability:git'
44
+ const REPLAY_TAB_ORDER = 40
45
+ const GIT_TAB_ORDER = 41
46
+
32
47
  // ── parts(scripts/build.mjs 拼接;顺序固定)───────────────────────
33
- // ── i18n(浏览器语言判定)──────────────────────────────────────────
34
- function isZh() {
35
- try {
36
- const lang = (navigator.language || 'en').toLowerCase()
37
- return lang.startsWith('zh')
38
- } catch {
39
- return false
48
+ /**
49
+ * 原生侧边栏页签注册助手(issue #187 批 1;scripts/build.mjs 拼接片段)。
50
+ *
51
+ * 原生结构:每个面板一个**页签类型**(sidebarRightTabs.register)+ 两个
52
+ * **keyed 席位**(slots.register,body 与 title)。id 全局唯一且是席位的
53
+ * key,一个 key 只能注册一个席位 —— 本插件有两个独立面板,因此每个 kind 用
54
+ * 自己的 id(沿用迁移前 better-sidebar 的 tab id)。
55
+ *
56
+ * 片段无 import/export,共享 client.src.js 的 factory 作用域:本函数的参数
57
+ * 全部由调用方(apply)注入,便于单测与阅读。
58
+ */
59
+ function registerNativeTab(ctx, tabs, slots, spec) {
60
+ ctx.effect(
61
+ () =>
62
+ tabs.register({
63
+ id: spec.id,
64
+ kind: spec.id,
65
+ title: spec.title,
66
+ guide: [{ order: spec.order, title: spec.title }],
67
+ }),
68
+ 'dsh-my-observability: ' + spec.label + ' tab',
69
+ )
70
+ ctx.effect(
71
+ () =>
72
+ slots.inject('sidebar.right.pane.tab', () =>
73
+ slots.register({ name: 'sidebar.right.pane.tab', key: spec.id }, spec.Body),
74
+ ),
75
+ 'dsh-my-observability: ' + spec.label + ' tab body',
76
+ )
77
+ ctx.effect(
78
+ () =>
79
+ slots.inject('sidebar.right.pane.tab.title', () =>
80
+ slots.register({ name: 'sidebar.right.pane.tab.title', key: spec.id }, spec.Title),
81
+ ),
82
+ 'dsh-my-observability: ' + spec.label + ' tab title',
83
+ )
84
+ }
85
+
86
+ /** 原生 tab title 席位:宿主给定标题优先,否则回退面板自带文案。 */
87
+ function nativeTabTitle(fallback) {
88
+ return function TabTitle(props) {
89
+ const info = typeof props.useTabInfo === 'function' ? props.useTabInfo() : undefined
90
+ return createElement('span', null, info?.tab?.title ?? fallback())
40
91
  }
41
92
  }
42
93
 
43
- const strings = {
44
- replayTitle: () => (isZh() ? '轨迹回放' : 'Trajectory'),
45
- resourceTitle: () => (isZh() ? '资源监控' : 'Resources'),
46
- resourceLoading: () => (isZh() ? '资源采样中…' : 'Sampling…'),
47
- resourceFile: () => (isZh() ? '审计文件' : 'Audit file'),
48
- resourceRate: () => (isZh() ? '写入速率' : 'Write rate'),
49
- resourceCpu: () => (isZh() ? 'CPU' : 'CPU'),
50
- resourceMem: () => (isZh() ? '内存' : 'Memory'),
51
- gitTitle: () => (isZh() ? 'Git 工具' : 'Git Tools'),
52
- allSessions: () => (isZh() ? '全部会话' : 'All sessions'),
53
- // ── 会话下拉可读性(issue #1xx:只能看到 UUID)────────────────────────
54
- eventCount: (n) => (isZh() ? `${n} 事件` : `${n} events`),
55
- sessionFallback: (shortId) => (isZh() ? `会话 ${shortId}` : `session ${shortId}`),
56
- filterAll: () => (isZh() ? '全部' : 'All'),
57
- filterStatus: () => (isZh() ? '状态' : 'Status'),
58
- filterLlm: () => (isZh() ? '模型流' : 'LLM'),
59
- filterTools: () => (isZh() ? '工具' : 'Tools'),
60
- filterPlugin: () => (isZh() ? '插件' : 'Plugins'),
61
- typePluginEvent: () => (isZh() ? '插件事件' : 'plugin event'),
62
- detailReason: () => (isZh() ? '原因' : 'reason'),
63
- emptyEvents: () => (isZh() ? '暂无审计事件' : 'No audit events yet'),
64
- emptyEventsHint: () =>
65
- isZh()
66
- ? '开始一段对话后,agent 的状态、模型流与工具调用会按时间记录在这里'
67
- : 'Start a conversation — agent status, LLM streams and tool calls are recorded here in time order',
68
- refresh: () => (isZh() ? '刷新' : 'Refresh'),
69
- retry: () => (isZh() ? '重试' : 'Retry'),
70
- loadError: () => (isZh() ? '加载失败' : 'Load failed'),
71
- typeAgentStatus: () => (isZh() ? 'agent 状态' : 'agent status'),
72
- typeLlmStream: () => (isZh() ? '模型流' : 'LLM stream'),
73
- typeToolCall: () => (isZh() ? '工具调用' : 'tool call'),
74
- typeToolResult: () => (isZh() ? '工具结果' : 'tool result'),
75
- phaseStart: () => (isZh() ? '开始' : 'start'),
76
- phaseEnd: () => (isZh() ? '结束' : 'end'),
77
- phaseError: () => (isZh() ? '错误' : 'error'),
78
- agentTop: () => (isZh() ? '顶层' : 'top'),
79
- agentSub: () => (isZh() ? '子代理' : 'subagent'),
80
- agentUnknown: () => (isZh() ? '未知' : 'unknown'),
81
- toolOk: () => (isZh() ? '成功' : 'ok'),
82
- toolFail: () => (isZh() ? '失败' : 'failed'),
83
- // 审计日志搜索 / 过滤 / 导出 / 统计
84
- searchPlaceholder: () => (isZh() ? '搜索工具名/参数/错误…' : 'Search tool/args/error…'),
85
- filterAllResult: () => (isZh() ? '全部结果' : 'All results'),
86
- filterSuccess: () => (isZh() ? '成功' : 'Ok'),
87
- filterFail: () => (isZh() ? '失败' : 'Failed'),
88
- timeStartLabel: () => (isZh() ? '开始' : 'Start'),
89
- timeEndLabel: () => (isZh() ? '结束' : 'End'),
90
- clearFilters: () => (isZh() ? '清除' : 'Clear'),
91
- exportJson: () => (isZh() ? '导出 JSON' : 'Export JSON'),
92
- exportCsv: () => (isZh() ? '导出 CSV' : 'Export CSV'),
93
- scopeSession: () => (isZh() ? '当前会话' : 'This session'),
94
- scopeAll: () => (isZh() ? '全部会话' : 'All sessions'),
95
- statsTitle: () => (isZh() ? '工具统计' : 'Tool stats'),
96
- statsTool: () => (isZh() ? '工具' : 'Tool'),
97
- statsCalls: () => (isZh() ? '调用' : 'Calls'),
98
- statsFailRate: () => (isZh() ? '失败率' : 'Fail rate'),
99
- statsEmpty: () => (isZh() ? '暂无工具调用数据' : 'No tool call data'),
100
- noMatches: () => (isZh() ? '无匹配事件' : 'No matching events'),
101
- exportFileName: () => (isZh() ? 'dsh-observability-审计导出' : 'dsh-observability-audit-export'),
102
- // Git 面板
103
- repoLabel: () => (isZh() ? '仓库路径' : 'Repo path'),
104
- repoPlaceholder: () => (isZh() ? '如 /path/to/project' : 'e.g. /path/to/project'),
105
- loadRepo: () => (isZh() ? '加载' : 'Load'),
106
- branch: () => (isZh() ? '分支' : 'Branch'),
107
- staged: () => (isZh() ? '已暂存' : 'staged'),
108
- unstaged: () => (isZh() ? '未暂存' : 'unstaged'),
109
- clean: () => (isZh() ? '工作区干净' : 'Working tree clean'),
110
- diffTitle: () => (isZh() ? '差异' : 'Diff'),
111
- showDiff: () => (isZh() ? '查看差异' : 'Show diff'),
112
- showStagedDiff: () => (isZh() ? '查看暂存差异' : 'Staged diff'),
113
- noChanges: () => (isZh() ? '没有变更' : 'No changes'),
114
- review: () => (isZh() ? '提交前审查' : 'Review'),
115
- reviewAi: () => (isZh() ? 'AI 审查' : 'AI review'),
116
- reviewResult: () => (isZh() ? '审查结果' : 'Review result'),
117
- reviewPass: () => (isZh() ? '未发现问题' : 'No issues found'),
118
- issues: (count) => (isZh() ? `${count} 个问题` : `${count} issue(s)`),
119
- commitTitle: () => (isZh() ? '类型化提交' : 'Typed commit'),
120
- commitType: () => (isZh() ? '类型' : 'Type'),
121
- commitScope: () => (isZh() ? '范围(可选)' : 'Scope (optional)'),
122
- commitDesc: () => (isZh() ? '描述' : 'Description'),
123
- commitBody: () => (isZh() ? '正文(可选)' : 'Body (optional)'),
124
- commit: () => (isZh() ? '提交' : 'Commit'),
125
- committed: () => (isZh() ? '已提交' : 'Committed'),
126
- commitError: () => (isZh() ? '提交失败' : 'Commit failed'),
127
- severityError: () => (isZh() ? '错误' : 'Error'),
128
- severityWarning: () => (isZh() ? '警告' : 'Warning'),
129
- severityInfo: () => (isZh() ? '提示' : 'Info'),
130
- aiVerdictApprove: () => (isZh() ? 'AI 结论:可以提交' : 'AI verdict: approve'),
131
- aiVerdictChanges: () => (isZh() ? 'AI 结论:建议修改' : 'AI verdict: changes'),
132
- aiFailed: () => (isZh() ? 'AI 审查不可用' : 'AI review unavailable'),
133
- loading: () => (isZh() ? '加载中…' : 'Loading…'),
134
- emptyDiff: () => (isZh() ? '(空)' : '(empty)'),
135
- noRepo: () => (isZh() ? '请输入仓库路径' : 'Enter a repo path'),
94
+ "use strict";
95
+ // ── i18n(浏览器语言判定)──────────────────────────────────────────
96
+ function isZh() {
97
+ try {
98
+ const lang = (navigator.language || 'en').toLowerCase();
99
+ return lang.startsWith('zh');
100
+ }
101
+ catch {
102
+ return false;
103
+ }
136
104
  }
105
+ const strings = {
106
+ replayTitle: () => (isZh() ? '轨迹回放' : 'Trajectory'),
107
+ resourceTitle: () => (isZh() ? '资源监控' : 'Resources'),
108
+ resourceLoading: () => (isZh() ? '资源采样中…' : 'Sampling…'),
109
+ resourceFile: () => (isZh() ? '审计文件' : 'Audit file'),
110
+ resourceRate: () => (isZh() ? '写入速率' : 'Write rate'),
111
+ resourceCpu: () => (isZh() ? 'CPU' : 'CPU'),
112
+ resourceMem: () => (isZh() ? '内存' : 'Memory'),
113
+ gitTitle: () => (isZh() ? 'Git 工具' : 'Git Tools'),
114
+ allSessions: () => (isZh() ? '全部会话' : 'All sessions'),
115
+ // ── 会话下拉可读性(issue #1xx:只能看到 UUID)────────────────────────
116
+ eventCount: (n) => (isZh() ? `${n} 事件` : `${n} events`),
117
+ sessionFallback: (shortId) => (isZh() ? `会话 ${shortId}` : `session ${shortId}`),
118
+ filterAll: () => (isZh() ? '全部' : 'All'),
119
+ filterStatus: () => (isZh() ? '状态' : 'Status'),
120
+ filterLlm: () => (isZh() ? '模型流' : 'LLM'),
121
+ filterTools: () => (isZh() ? '工具' : 'Tools'),
122
+ filterPlugin: () => (isZh() ? '插件' : 'Plugins'),
123
+ typePluginEvent: () => (isZh() ? '插件事件' : 'plugin event'),
124
+ detailReason: () => (isZh() ? '原因' : 'reason'),
125
+ emptyEvents: () => (isZh() ? '暂无审计事件' : 'No audit events yet'),
126
+ emptyEventsHint: () => isZh()
127
+ ? '开始一段对话后,agent 的状态、模型流与工具调用会按时间记录在这里'
128
+ : 'Start a conversation — agent status, LLM streams and tool calls are recorded here in time order',
129
+ refresh: () => (isZh() ? '刷新' : 'Refresh'),
130
+ retry: () => (isZh() ? '重试' : 'Retry'),
131
+ loadError: () => (isZh() ? '加载失败' : 'Load failed'),
132
+ typeAgentStatus: () => (isZh() ? 'agent 状态' : 'agent status'),
133
+ typeLlmStream: () => (isZh() ? '模型流' : 'LLM stream'),
134
+ typeToolCall: () => (isZh() ? '工具调用' : 'tool call'),
135
+ typeToolResult: () => (isZh() ? '工具结果' : 'tool result'),
136
+ phaseStart: () => (isZh() ? '开始' : 'start'),
137
+ phaseEnd: () => (isZh() ? '结束' : 'end'),
138
+ phaseError: () => (isZh() ? '错误' : 'error'),
139
+ agentTop: () => (isZh() ? '顶层' : 'top'),
140
+ agentSub: () => (isZh() ? '子代理' : 'subagent'),
141
+ agentUnknown: () => (isZh() ? '未知' : 'unknown'),
142
+ toolOk: () => (isZh() ? '成功' : 'ok'),
143
+ toolFail: () => (isZh() ? '失败' : 'failed'),
144
+ // 审计日志搜索 / 过滤 / 导出 / 统计
145
+ searchPlaceholder: () => (isZh() ? '搜索工具名/参数/错误…' : 'Search tool/args/error…'),
146
+ filterAllResult: () => (isZh() ? '全部结果' : 'All results'),
147
+ filterSuccess: () => (isZh() ? '成功' : 'Ok'),
148
+ filterFail: () => (isZh() ? '失败' : 'Failed'),
149
+ timeStartLabel: () => (isZh() ? '开始' : 'Start'),
150
+ timeEndLabel: () => (isZh() ? '结束' : 'End'),
151
+ clearFilters: () => (isZh() ? '清除' : 'Clear'),
152
+ exportJson: () => (isZh() ? '导出 JSON' : 'Export JSON'),
153
+ exportCsv: () => (isZh() ? '导出 CSV' : 'Export CSV'),
154
+ scopeSession: () => (isZh() ? '当前会话' : 'This session'),
155
+ scopeAll: () => (isZh() ? '全部会话' : 'All sessions'),
156
+ statsTitle: () => (isZh() ? '工具统计' : 'Tool stats'),
157
+ statsTool: () => (isZh() ? '工具' : 'Tool'),
158
+ statsCalls: () => (isZh() ? '调用' : 'Calls'),
159
+ statsFailRate: () => (isZh() ? '失败率' : 'Fail rate'),
160
+ statsEmpty: () => (isZh() ? '暂无工具调用数据' : 'No tool call data'),
161
+ noMatches: () => (isZh() ? '无匹配事件' : 'No matching events'),
162
+ exportFileName: () => (isZh() ? 'dsh-observability-审计导出' : 'dsh-observability-audit-export'),
163
+ // Git 面板
164
+ repoLabel: () => (isZh() ? '仓库路径' : 'Repo path'),
165
+ repoPlaceholder: () => (isZh() ? '如 /path/to/project' : 'e.g. /path/to/project'),
166
+ loadRepo: () => (isZh() ? '加载' : 'Load'),
167
+ branch: () => (isZh() ? '分支' : 'Branch'),
168
+ staged: () => (isZh() ? '已暂存' : 'staged'),
169
+ unstaged: () => (isZh() ? '未暂存' : 'unstaged'),
170
+ clean: () => (isZh() ? '工作区干净' : 'Working tree clean'),
171
+ diffTitle: () => (isZh() ? '差异' : 'Diff'),
172
+ showDiff: () => (isZh() ? '查看差异' : 'Show diff'),
173
+ showStagedDiff: () => (isZh() ? '查看暂存差异' : 'Staged diff'),
174
+ noChanges: () => (isZh() ? '没有变更' : 'No changes'),
175
+ review: () => (isZh() ? '提交前审查' : 'Review'),
176
+ reviewAi: () => (isZh() ? 'AI 审查' : 'AI review'),
177
+ reviewResult: () => (isZh() ? '审查结果' : 'Review result'),
178
+ reviewPass: () => (isZh() ? '未发现问题' : 'No issues found'),
179
+ issues: (count) => (isZh() ? `${count} 个问题` : `${count} issue(s)`),
180
+ commitTitle: () => (isZh() ? '类型化提交' : 'Typed commit'),
181
+ commitType: () => (isZh() ? '类型' : 'Type'),
182
+ commitScope: () => (isZh() ? '范围(可选)' : 'Scope (optional)'),
183
+ commitDesc: () => (isZh() ? '描述' : 'Description'),
184
+ commitBody: () => (isZh() ? '正文(可选)' : 'Body (optional)'),
185
+ commit: () => (isZh() ? '提交' : 'Commit'),
186
+ committed: () => (isZh() ? '已提交' : 'Committed'),
187
+ commitError: () => (isZh() ? '提交失败' : 'Commit failed'),
188
+ severityError: () => (isZh() ? '错误' : 'Error'),
189
+ severityWarning: () => (isZh() ? '警告' : 'Warning'),
190
+ severityInfo: () => (isZh() ? '提示' : 'Info'),
191
+ aiVerdictApprove: () => (isZh() ? 'AI 结论:可以提交' : 'AI verdict: approve'),
192
+ aiVerdictChanges: () => (isZh() ? 'AI 结论:建议修改' : 'AI verdict: changes'),
193
+ aiFailed: () => (isZh() ? 'AI 审查不可用' : 'AI review unavailable'),
194
+ loading: () => (isZh() ? '加载中…' : 'Loading…'),
195
+ emptyDiff: () => (isZh() ? '(空)' : '(empty)'),
196
+ noRepo: () => (isZh() ? '请输入仓库路径' : 'Enter a repo path'),
197
+ };
137
198
 
138
199
  // ── shared icons (inline, stroke=currentColor, matching better-sidebar) ──
139
200
  // Single source of truth for the plugin UI icon set (issue #54 阶段 0).
@@ -304,6 +365,33 @@ const icon = {
304
365
  ],
305
366
  size,
306
367
  ),
368
+ // 箭头向上(更新图标):向上的箭头,表示更新操作
369
+ arrowUp: (size = 16) =>
370
+ iconSvg(
371
+ [
372
+ createElement('line', { x1: 12, y1: 19, x2: 12, y2: 5 }),
373
+ createElement('polyline', { points: '5 12 12 5 19 12' }),
374
+ ],
375
+ size,
376
+ ),
377
+ // 电源关(禁用图标):圆形电源按钮,表示禁用操作
378
+ powerOff: (size = 16) =>
379
+ iconSvg(
380
+ [
381
+ createElement('path', { d: 'M18.36 6.64a9 9 0 1 1-12.73 0' }),
382
+ createElement('line', { x1: 12, y1: 2, x2: 12, y2: 12 }),
383
+ ],
384
+ size,
385
+ ),
386
+ // 电源开(启用图标):圆形电源按钮,表示启用操作
387
+ powerOn: (size = 16) =>
388
+ iconSvg(
389
+ [
390
+ createElement('path', { d: 'M18.36 6.64a9 9 0 1 1-12.73 0' }),
391
+ createElement('line', { x1: 12, y1: 2, x2: 12, y2: 12 }),
392
+ ],
393
+ size,
394
+ ),
307
395
  }
308
396
 
309
397
  // Common-language / file-type badges (issue #24): brand fill + contrast
@@ -473,1432 +561,1079 @@ const fileIconByExt = (ext, size = 14) => {
473
561
  * - 不读取 strings —— 涉及界面文案的默认值集中在此,client 如需 i18n 覆盖
474
562
  * 通过参数传入。
475
563
  */
476
-
477
564
  /** 事件类型 → 中文标签(CSV 默认;client 可传 labels 覆盖)。非导出常量。 */
478
565
  const DEFAULT_CSV_LABELS = Object.freeze({
479
- time: '时间',
480
- type: '类型',
481
- tool: '工具',
482
- result: '结果',
483
- typeMap: Object.freeze({
484
- agent_status: 'agent 状态',
485
- llm_stream: '模型流',
486
- tool_call: '工具调用',
487
- tool_result: '工具结果',
488
- }),
489
- ok: '成功',
490
- fail: '失败',
491
- error: '错误',
492
- })
493
-
494
- const MAX_STATS_TOP = 50
495
-
566
+ time: '时间',
567
+ type: '类型',
568
+ tool: '工具',
569
+ result: '结果',
570
+ typeMap: Object.freeze({
571
+ agent_status: 'agent 状态',
572
+ llm_stream: '模型流',
573
+ tool_call: '工具调用',
574
+ tool_result: '工具结果',
575
+ }),
576
+ ok: '成功',
577
+ fail: '失败',
578
+ error: '错误',
579
+ });
580
+ const MAX_STATS_TOP = 50;
496
581
  /** 两位补零。 */
497
582
  function pad2(n) {
498
- return String(n).padStart(2, '0')
583
+ return String(n).padStart(2, '0');
499
584
  }
500
-
501
585
  /** 毫秒时间戳 → `YYYY-MM-DD HH:MM:SS`(本地时区);非法输入返回空串。 */
502
586
  function formatTime(time) {
503
- if (typeof time !== 'number' || !Number.isFinite(time)) return ''
504
- const d = new Date(time)
505
- if (Number.isNaN(d.getTime())) return ''
506
- const date = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
507
- const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
508
- return `${date} ${clock}`
587
+ if (typeof time !== 'number' || !Number.isFinite(time))
588
+ return '';
589
+ const d = new Date(time);
590
+ if (Number.isNaN(d.getTime()))
591
+ return '';
592
+ const date = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
593
+ const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
594
+ return `${date} ${clock}`;
509
595
  }
510
-
511
596
  /** agent 状态事件的搜索片段。 */
512
597
  function agentStatusParts(data) {
513
- return [data.status, data.agentType].filter((part) => typeof part === 'string')
598
+ return [data.status, data.agentType].filter((part) => typeof part === 'string');
514
599
  }
515
-
516
600
  /** 模型流事件的搜索片段(含错误消息)。 */
517
601
  function llmParts(data) {
518
- const parts = [data.phase]
519
- if (typeof data.message === 'string' && data.message !== '') parts.push(data.message)
520
- return parts
602
+ const parts = [data.phase];
603
+ if (typeof data.message === 'string' && data.message !== '')
604
+ parts.push(data.message);
605
+ return parts;
521
606
  }
522
-
523
607
  /** 工具调用事件的搜索片段(工具名 + 参数键 + 参数摘要)。 */
524
608
  function toolCallParts(data) {
525
- const parts = []
526
- if (typeof data.name === 'string') parts.push(data.name)
527
- if (Array.isArray(data.args?.keys)) parts.push(...data.args.keys)
528
- if (typeof data.args?.summary === 'string' && data.args.summary !== '') parts.push(data.args.summary)
529
- return parts
609
+ const parts = [];
610
+ if (typeof data.name === 'string')
611
+ parts.push(data.name);
612
+ if (Array.isArray(data.args?.keys))
613
+ parts.push(...data.args.keys);
614
+ if (typeof data.args?.summary === 'string' && data.args.summary !== '')
615
+ parts.push(data.args.summary);
616
+ return parts;
530
617
  }
531
-
532
618
  /** 工具结果事件的搜索片段(工具名 + 成败)。 */
533
619
  function toolResultParts(data) {
534
- const parts = []
535
- if (typeof data.name === 'string') parts.push(data.name)
536
- parts.push(data.ok === false ? '失败' : '成功')
537
- return parts
620
+ const parts = [];
621
+ if (typeof data.name === 'string')
622
+ parts.push(data.name);
623
+ parts.push(data.ok === false ? '失败' : '成功');
624
+ return parts;
538
625
  }
539
-
540
626
  /** 插件事件(issue #154)的搜索片段(插件名/事件名/动作/原因/参数值)。 */
541
627
  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)
628
+ const parts = [];
629
+ if (typeof data.plugin === 'string')
630
+ parts.push(data.plugin);
631
+ if (typeof data.event === 'string')
632
+ parts.push(data.event);
633
+ if (typeof data.action === 'string')
634
+ parts.push(data.action);
635
+ if (typeof data.reason === 'string')
636
+ parts.push(data.reason);
637
+ if (data.params !== null && typeof data.params === 'object') {
638
+ for (const value of Object.values(data.params)) {
639
+ if (typeof value === 'string' && value !== '')
640
+ parts.push(value);
641
+ }
550
642
  }
551
- }
552
- return parts
643
+ return parts;
553
644
  }
554
-
555
645
  /** 事件类型 → 搜索片段收集函数(查表消分支)。 */
556
646
  const PARTS_COLLECTORS = {
557
- agent_status: agentStatusParts,
558
- llm_stream: llmParts,
559
- tool_call: toolCallParts,
560
- tool_result: toolResultParts,
561
- plugin_event: pluginEventParts,
562
- }
563
-
647
+ agent_status: agentStatusParts,
648
+ llm_stream: llmParts,
649
+ tool_call: toolCallParts,
650
+ tool_result: toolResultParts,
651
+ plugin_event: pluginEventParts,
652
+ };
564
653
  /** 提取事件可用于关键词匹配的文本(工具名/参数摘要/错误信息/状态/阶段等)。 */
565
654
  function searchableText(event) {
566
- const data = event && event.data ? event.data : {}
567
- const parts = [event?.type, event?.sessionId]
568
- const collector = PARTS_COLLECTORS[event?.type]
569
- if (collector !== undefined) parts.push(...collector(data))
570
- return parts
571
- .filter((part) => typeof part === 'string')
572
- .join(' ')
573
- .toLowerCase()
655
+ const data = event && event.data ? event.data : {};
656
+ const parts = [event?.type, event?.sessionId];
657
+ const collector = PARTS_COLLECTORS[event?.type];
658
+ if (collector !== undefined)
659
+ parts.push(...collector(data));
660
+ return parts
661
+ .filter((part) => typeof part === 'string')
662
+ .join(' ')
663
+ .toLowerCase();
574
664
  }
575
-
576
665
  /** 事件是否命中关键词(不区分大小写;空关键词视为命中全部)。 */
577
666
  function matchesKeyword(event, keyword) {
578
- const kw = String(keyword ?? '')
579
- .trim()
580
- .toLowerCase()
581
- if (kw === '') return true
582
- return searchableText(event).includes(kw)
667
+ const kw = String(keyword ?? '')
668
+ .trim()
669
+ .toLowerCase();
670
+ if (kw === '')
671
+ return true;
672
+ return searchableText(event).includes(kw);
583
673
  }
584
-
585
674
  /** 返回 `true` 表示事件具备失败语义(工具失败 / 模型流出错)。 */
586
675
  function isFailEvent(event) {
587
- if (event?.type === 'tool_result') return event.data?.ok === false
588
- if (event?.type === 'llm_stream') return event.data?.phase === 'error'
589
- return false
676
+ if (event?.type === 'tool_result')
677
+ return event.data?.ok === false;
678
+ if (event?.type === 'llm_stream')
679
+ return event.data?.phase === 'error';
680
+ return false;
590
681
  }
591
-
592
682
  /** 归一化过滤条件(时间转为闭区间数值;空值透传)。 */
593
683
  function normalizeCriteria(criteria) {
594
- const start =
595
- typeof criteria.timeStart === 'number' && Number.isFinite(criteria.timeStart) ? criteria.timeStart : undefined
596
- const end = typeof criteria.timeEnd === 'number' && Number.isFinite(criteria.timeEnd) ? criteria.timeEnd : undefined
597
- return { type: criteria.type ?? '', keyword: criteria.keyword ?? '', result: criteria.result ?? '', start, end }
684
+ const start = typeof criteria.timeStart === 'number' && Number.isFinite(criteria.timeStart) ? criteria.timeStart : undefined;
685
+ const end = typeof criteria.timeEnd === 'number' && Number.isFinite(criteria.timeEnd) ? criteria.timeEnd : undefined;
686
+ return { type: criteria.type ?? '', keyword: criteria.keyword ?? '', result: criteria.result ?? '', start, end };
598
687
  }
599
-
600
688
  /** 类型过滤('tool' 表示 tool_call + tool_result;'plugin' 表示 plugin_event)。 */
601
689
  function passType(type, filterType) {
602
- if (filterType === '') return true
603
- if (filterType === 'tool') return type === 'tool_call' || type === 'tool_result'
604
- if (filterType === 'plugin') return type === 'plugin_event'
605
- return type === filterType
690
+ if (filterType === '')
691
+ return true;
692
+ if (filterType === 'tool')
693
+ return type === 'tool_call' || type === 'tool_result';
694
+ if (filterType === 'plugin')
695
+ return type === 'plugin_event';
696
+ return type === filterType;
606
697
  }
607
-
608
698
  /** 时间范围闭区间。 */
609
699
  function passTime(time, start, end) {
610
- if (start !== undefined && time < start) return false
611
- if (end !== undefined && time > end) return false
612
- return true
700
+ if (start !== undefined && time < start)
701
+ return false;
702
+ if (end !== undefined && time > end)
703
+ return false;
704
+ return true;
613
705
  }
614
-
615
706
  /** 成功/失败过滤:只作用于有成败语义的事件,其余事件透传。 */
616
707
  function passResult(event, result) {
617
- if (result === '') return true
618
- if (result === 'success') return !isFailEvent(event)
619
- if (result === 'fail') return isFailEvent(event)
620
- return true
708
+ if (result === '')
709
+ return true;
710
+ if (result === 'success')
711
+ return !isFailEvent(event);
712
+ if (result === 'fail')
713
+ return isFailEvent(event);
714
+ return true;
621
715
  }
622
-
623
716
  /** 组合过滤:类型(tool 表示 tool_call+tool_result)+ 时间范围 + 成功/失败 + 关键词。
624
717
  * criteria: { type, timeStart, timeEnd, result, keyword } */
625
718
  function applyAuditFilter(events, criteria = {}) {
626
- const ctx = normalizeCriteria(criteria)
627
- return (events ?? []).filter(
628
- (event) =>
629
- passType(event.type, ctx.type) &&
630
- passTime(event.time, ctx.start, ctx.end) &&
631
- passResult(event, ctx.result) &&
632
- matchesKeyword(event, ctx.keyword),
633
- )
719
+ const ctx = normalizeCriteria(criteria);
720
+ return (events ?? []).filter((event) => passType(event.type, ctx.type) &&
721
+ passTime(event.time, ctx.start, ctx.end) &&
722
+ passResult(event, ctx.result) &&
723
+ matchesKeyword(event, ctx.keyword));
634
724
  }
635
-
636
725
  /** CSV 单元格转义:含逗号/引号/换行时用双引号包裹并转义内嵌引号。 */
637
726
  function csvCell(value) {
638
- const s = value === null || value === undefined ? '' : String(value)
639
- return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
727
+ const s = value === null || value === undefined ? '' : String(value);
728
+ return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
640
729
  }
641
-
642
730
  /** 事件的工具名(仅 tool_call/tool_result;否则空)。 */
643
731
  function toolNameOf(event) {
644
- if (event?.type === 'tool_call' || event?.type === 'tool_result') return String(event.data?.name ?? '')
645
- return ''
732
+ if (event?.type === 'tool_call' || event?.type === 'tool_result')
733
+ return String(event.data?.name ?? '');
734
+ return '';
646
735
  }
647
-
648
736
  /** 事件的结果摘要(成功/失败/错误;其余空)。 */
649
737
  function resultTextOf(event, labels = DEFAULT_CSV_LABELS) {
650
- if (event?.type === 'tool_result') return event.data?.ok === false ? labels.fail : labels.ok
651
- if (event?.type === 'llm_stream' && event.data?.phase === 'error') return labels.error
652
- return ''
738
+ if (event?.type === 'tool_result')
739
+ return event.data?.ok === false ? labels.fail : labels.ok;
740
+ if (event?.type === 'llm_stream' && event.data?.phase === 'error')
741
+ return labels.error;
742
+ return '';
653
743
  }
654
-
655
744
  /** 生成 CSV 摘要(表头:时间/类型/工具/结果)。labels 可覆盖默认中文。
656
745
  * 返回不含换行结尾符的 CSV 文本。 */
657
746
  function auditToCsv(events, labels = DEFAULT_CSV_LABELS) {
658
- const typeMap = labels.typeMap ?? {}
659
- const header = [labels.time, labels.type, labels.tool, labels.result]
660
- const lines = [header.map(csvCell).join(',')]
661
- for (const event of events ?? []) {
662
- const typeLabel = typeMap[event.type] ?? String(event.type)
663
- const row = [formatTime(event.time), typeLabel, toolNameOf(event), resultTextOf(event, labels)]
664
- lines.push(row.map(csvCell).join(','))
665
- }
666
- return lines.join('\n')
747
+ const typeMap = labels.typeMap ?? {};
748
+ const header = [labels.time, labels.type, labels.tool, labels.result];
749
+ const lines = [header.map(csvCell).join(',')];
750
+ for (const event of (events ?? [])) {
751
+ const typeLabel = typeMap[event.type] ?? String(event.type);
752
+ const row = [formatTime(event.time), typeLabel, toolNameOf(event), resultTextOf(event, labels)];
753
+ lines.push(row.map(csvCell).join(','));
754
+ }
755
+ return lines.join('\n');
667
756
  }
668
-
669
757
  /** 生成 JSON 完整数据(缩进默认 2)。 */
670
758
  function auditToJson(events, space = 2) {
671
- return JSON.stringify(events ?? [], null, space)
759
+ return JSON.stringify(events ?? [], null, space);
672
760
  }
673
-
674
761
  /** 事件是否为工具类(tool_call / tool_result)。 */
675
762
  function isToolEvent(event) {
676
- return event?.type === 'tool_call' || event?.type === 'tool_result'
763
+ return event?.type === 'tool_call' || event?.type === 'tool_result';
677
764
  }
678
-
679
765
  /** 把单条工具事件计入聚合(调用次数 / 失败次数)。 */
680
766
  function bumpTool(byTool, event, name) {
681
- const entry = byTool.get(name) ?? { tool: name, calls: 0, fails: 0 }
682
- if (event.type === 'tool_call') entry.calls += 1
683
- if (event.type === 'tool_result' && event.data?.ok === false) entry.fails += 1
684
- byTool.set(name, entry)
767
+ const entry = byTool.get(name) ?? { tool: name, calls: 0, fails: 0 };
768
+ if (event.type === 'tool_call')
769
+ entry.calls += 1;
770
+ if (event.type === 'tool_result' && event.data?.ok === false)
771
+ entry.fails += 1;
772
+ byTool.set(name, entry);
685
773
  }
686
-
687
774
  /** 按工具名聚合调用次数与失败次数。 */
688
775
  function aggregateToolStats(events) {
689
- const byTool = new Map()
690
- for (const event of events ?? []) {
691
- if (!isToolEvent(event)) continue
692
- const name = String(event.data?.name ?? '')
693
- if (name === '') continue
694
- bumpTool(byTool, event, name)
695
- }
696
- return byTool
776
+ const byTool = new Map();
777
+ for (const event of (events ?? [])) {
778
+ if (!isToolEvent(event))
779
+ continue;
780
+ const name = String(event.data?.name ?? '');
781
+ if (name === '')
782
+ continue;
783
+ bumpTool(byTool, event, name);
784
+ }
785
+ return byTool;
697
786
  }
698
-
699
787
  /** 聚合结果 → 排序 + 失败率列表。 */
700
788
  function rankTools(byTool) {
701
- return [...byTool.values()]
702
- .map((entry) => ({ ...entry, failRate: entry.calls > 0 ? entry.fails / entry.calls : 0 }))
703
- .sort((a, b) => b.calls - a.calls || b.fails - a.fails || a.tool.localeCompare(b.tool))
789
+ return [...byTool.values()]
790
+ .map((entry) => ({ ...entry, failRate: entry.calls > 0 ? entry.fails / entry.calls : 0 }))
791
+ .sort((a, b) => b.calls - a.calls || b.fails - a.fails || a.tool.localeCompare(b.tool));
704
792
  }
705
-
706
793
  /** 工具调用统计:每个工具调用次数 + 失败率(topN 截断,默认 5)。
707
794
  * 返回 [{ tool, calls, fails, failRate }] 按调用次数降序。 */
708
795
  function computeToolStats(events, topN = 5) {
709
- const n = typeof topN === 'number' && topN > 0 ? Math.min(topN, MAX_STATS_TOP) : 5
710
- return rankTools(aggregateToolStats(events)).slice(0, n)
796
+ const n = typeof topN === 'number' && topN > 0 ? Math.min(topN, MAX_STATS_TOP) : 5;
797
+ return rankTools(aggregateToolStats(events)).slice(0, n);
711
798
  }
712
-
713
799
  /** 把 text 按 keyword 切成 [ { text, hit } ] 分段(用于命中关键词高亮)。
714
800
  * 空关键词返回整段未命中。 */
715
801
  function highlightSegments(text, keyword) {
716
- const raw = String(text ?? '')
717
- const kw = String(keyword ?? '')
718
- .trim()
719
- .toLowerCase()
720
- if (kw === '') return [{ text: raw, hit: false }]
721
- const lower = raw.toLowerCase()
722
- const out = []
723
- let cursor = 0
724
- for (;;) {
725
- const idx = lower.indexOf(kw, cursor)
726
- if (idx === -1) {
727
- if (cursor < raw.length) out.push({ text: raw.slice(cursor), hit: false })
728
- break
802
+ const raw = String(text ?? '');
803
+ const kw = String(keyword ?? '')
804
+ .trim()
805
+ .toLowerCase();
806
+ if (kw === '')
807
+ return [{ text: raw, hit: false }];
808
+ const lower = raw.toLowerCase();
809
+ const out = [];
810
+ let cursor = 0;
811
+ for (;;) {
812
+ const idx = lower.indexOf(kw, cursor);
813
+ if (idx === -1) {
814
+ if (cursor < raw.length)
815
+ out.push({ text: raw.slice(cursor), hit: false });
816
+ break;
817
+ }
818
+ if (idx > cursor)
819
+ out.push({ text: raw.slice(cursor, idx), hit: false });
820
+ out.push({ text: raw.slice(idx, idx + kw.length), hit: true });
821
+ cursor = idx + kw.length;
729
822
  }
730
- if (idx > cursor) out.push({ text: raw.slice(cursor, idx), hit: false })
731
- out.push({ text: raw.slice(idx, idx + kw.length), hit: true })
732
- cursor = idx + kw.length
733
- }
734
- return out.length === 0 ? [{ text: raw, hit: false }] : out
823
+ return out.length === 0 ? [{ text: raw, hit: false }] : out;
735
824
  }
736
825
 
737
- // ── 轨迹回放面板(时间轴)──────────────────────────────────────────
826
+ "use strict";
827
+ // ── 轨迹回放面板(时间轴)──────────────────────────────────────────
738
828
  // 拆分的审计视图组件(搜索/组合过滤/导出/统计/高亮/hook)位于
739
829
  // replay-ext.js 片段(见 client.src.js 占位符顺序)。
740
-
741
830
  /** 请求插件 API(非 2xx 抛错;返回响应 JSON 的 value 字段)。 */
742
831
  function apiJson(path, options) {
743
- return fetch(path, options).then(async (res) => {
744
- const data = await res.json()
745
- if (!res.ok) throw new Error(data.error?.message || `HTTP ${res.status}`)
746
- return data.value
747
- })
832
+ return fetch(path, options).then(async (res) => {
833
+ const data = await res.json();
834
+ if (!res.ok)
835
+ throw new Error(data.error?.message || `HTTP ${res.status}`);
836
+ return data.value;
837
+ });
748
838
  }
749
-
750
839
  /** 事件类型 → 中文标签。 */
751
840
  function typeLabel(event) {
752
- switch (event.type) {
753
- case 'agent_status':
754
- return strings.typeAgentStatus()
755
- case 'llm_stream':
756
- return strings.typeLlmStream()
757
- case 'tool_call':
758
- return strings.typeToolCall()
759
- case 'tool_result':
760
- return strings.typeToolResult()
761
- case 'plugin_event':
762
- return strings.typePluginEvent()
763
- default:
764
- return event.type
765
- }
841
+ switch (event.type) {
842
+ case 'agent_status':
843
+ return strings.typeAgentStatus();
844
+ case 'llm_stream':
845
+ return strings.typeLlmStream();
846
+ case 'tool_call':
847
+ return strings.typeToolCall();
848
+ case 'tool_result':
849
+ return strings.typeToolResult();
850
+ case 'plugin_event':
851
+ return strings.typePluginEvent();
852
+ default:
853
+ return event.type;
854
+ }
766
855
  }
767
-
768
856
  /** 事件类型 → 视觉类别(徽标/图标/节点共用,颜色语义一致):
769
857
  * status=info / llm=warn / call=accent / result=success / fail=danger /
770
858
  * plugin=info(插件事件复用 info 色,图标区分)。 */
771
859
  function typeKind(event) {
772
- if (event.type === 'agent_status') return 'status'
773
- if (event.type === 'llm_stream') return 'llm'
774
- if (event.type === 'tool_call') return 'call'
775
- if (event.type === 'plugin_event') return 'plugin'
776
- return event.data?.ok === false ? 'fail' : 'result'
860
+ if (event.type === 'agent_status')
861
+ return 'status';
862
+ if (event.type === 'llm_stream')
863
+ return 'llm';
864
+ if (event.type === 'tool_call')
865
+ return 'call';
866
+ if (event.type === 'plugin_event')
867
+ return 'plugin';
868
+ return event.data?.ok === false ? 'fail' : 'result';
777
869
  }
778
-
779
870
  /** 事件类型 → 类型图标(共享线性图标集,stroke=currentColor)。 */
780
871
  function typeIcon(event) {
781
- const kind = typeKind(event)
782
- if (kind === 'status') return icon.clock(15)
783
- if (kind === 'llm') return icon.file(15)
784
- if (kind === 'call') return icon.external(15)
785
- if (kind === 'plugin') return icon.alert(15)
786
- if (kind === 'fail') return icon.close(15)
787
- return icon.check(15)
872
+ const kind = typeKind(event);
873
+ if (kind === 'status')
874
+ return icon.clock(15);
875
+ if (kind === 'llm')
876
+ return icon.file(15);
877
+ if (kind === 'call')
878
+ return icon.external(15);
879
+ if (kind === 'plugin')
880
+ return icon.alert(15);
881
+ if (kind === 'fail')
882
+ return icon.close(15);
883
+ return icon.check(15);
788
884
  }
789
-
790
885
  /** 时间戳 → HH:MM:SS。 */
791
886
  function timeText(time) {
792
- try {
793
- const date = new Date(time)
794
- const pad = (n) => String(n).padStart(2, '0')
795
- return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
796
- } catch {
797
- return ''
798
- }
887
+ try {
888
+ const date = new Date(time);
889
+ const pad = (n) => String(n).padStart(2, '0');
890
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
891
+ }
892
+ catch {
893
+ return '';
894
+ }
799
895
  }
800
-
801
896
  /** agent 类型标记 → 中文。 */
802
897
  function agentTypeText(agentType) {
803
- if (agentType === 'top') return strings.agentTop()
804
- if (agentType === 'subagent') return strings.agentSub()
805
- return strings.agentUnknown()
898
+ if (agentType === 'top')
899
+ return strings.agentTop();
900
+ if (agentType === 'subagent')
901
+ return strings.agentSub();
902
+ return strings.agentUnknown();
806
903
  }
807
-
808
904
  /** 模型流阶段 → 中文。 */
809
905
  function phaseText(phase) {
810
- if (phase === 'start') return strings.phaseStart()
811
- if (phase === 'end') return strings.phaseEnd()
812
- if (phase === 'error') return strings.phaseError()
813
- return phase
906
+ if (phase === 'start')
907
+ return strings.phaseStart();
908
+ if (phase === 'end')
909
+ return strings.phaseEnd();
910
+ if (phase === 'error')
911
+ return strings.phaseError();
912
+ return phase;
814
913
  }
815
-
816
914
  /** agent 状态事件摘要。 */
817
915
  function agentMeta(data) {
818
- return `状态 ${data.status} · ${agentTypeText(data.agentType)}`
916
+ return `状态 ${data.status} · ${agentTypeText(data.agentType)}`;
819
917
  }
820
-
821
918
  /** 模型流事件摘要(开始/结束/错误 + 统计)。 */
822
919
  function llmMeta(data) {
823
- const stats = data.phase === 'start' ? '' : ` · ${data.chunks} chunks / ${data.chars} chars / ${data.ms}ms`
824
- const error = data.message !== undefined ? `:${data.message}` : ''
825
- return `${phaseText(data.phase)}${stats}${error}`
920
+ const stats = data.phase === 'start' ? '' : ` · ${data.chunks} chunks / ${data.chars} chars / ${data.ms}ms`;
921
+ const error = data.message !== undefined ? `:${data.message}` : '';
922
+ return `${phaseText(data.phase)}${stats}${error}`;
826
923
  }
827
-
828
924
  /** 工具调用事件摘要(名称 + 参数摘要)。 */
829
925
  function toolCallMeta(data) {
830
- const args = data.args && data.args.summary !== undefined ? ` — ${data.args.summary}` : ''
831
- return `${data.name}${args}`
926
+ const args = data.args && data.args.summary !== undefined ? ` — ${data.args.summary}` : '';
927
+ return `${data.name}${args}`;
832
928
  }
833
-
834
929
  /** 工具结果事件摘要(名称 + 成败 + 耗时)。 */
835
930
  function toolResultMeta(data) {
836
- const result = data.ok === false ? strings.toolFail() : strings.toolOk()
837
- return `${data.name} · ${result} · ${data.ms}ms`
931
+ const result = data.ok === false ? strings.toolFail() : strings.toolOk();
932
+ return `${data.name} · ${result} · ${data.ms}ms`;
838
933
  }
839
-
840
934
  /** 插件事件摘要(插件名 · 事件名 · 动作;issue #154)。 */
841
935
  function pluginEventMeta(data) {
842
- const action = typeof data.action === 'string' && data.action !== '' ? data.action : data.event
843
- return `${data.plugin} · ${data.event} · ${action}`
936
+ const action = typeof data.action === 'string' && data.action !== '' ? data.action : data.event;
937
+ return `${data.plugin} · ${data.event} · ${action}`;
844
938
  }
845
-
846
939
  /** 事件 → 摘要文本(单行,尽力而为)。 */
847
940
  function eventMeta(event) {
848
- const data = event.data || {}
849
- if (event.type === 'agent_status') return agentMeta(data)
850
- if (event.type === 'llm_stream') return llmMeta(data)
851
- if (event.type === 'tool_call') return toolCallMeta(data)
852
- if (event.type === 'tool_result') return toolResultMeta(data)
853
- if (event.type === 'plugin_event') return pluginEventMeta(data)
854
- return ''
941
+ const data = event.data || {};
942
+ if (event.type === 'agent_status')
943
+ return agentMeta(data);
944
+ if (event.type === 'llm_stream')
945
+ return llmMeta(data);
946
+ if (event.type === 'tool_call')
947
+ return toolCallMeta(data);
948
+ if (event.type === 'tool_result')
949
+ return toolResultMeta(data);
950
+ if (event.type === 'plugin_event')
951
+ return pluginEventMeta(data);
952
+ return '';
855
953
  }
856
-
857
954
  /** 插件事件详情行(原因 + 参数键值对;非插件事件返回 null 不可展开)。 */
858
955
  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
- )
956
+ if (event?.type !== 'plugin_event')
957
+ return null;
958
+ const data = event.data || {};
959
+ const rows = [];
960
+ if (typeof data.reason === 'string' && data.reason !== '') {
961
+ rows.push(createElement('div', { key: 'reason', className: 'dsh-my-observability-detail-row' }, createElement('span', { className: 'dsh-my-observability-detail-key' }, strings.detailReason()), createElement('span', { className: 'dsh-my-observability-detail-value' }, data.reason)));
882
962
  }
883
- }
884
- return rows.length > 0 ? rows : null
963
+ if (data.params !== null && typeof data.params === 'object') {
964
+ for (const [key, value] of Object.entries(data.params)) {
965
+ rows.push(createElement('div', { key, className: 'dsh-my-observability-detail-row' }, createElement('span', { className: 'dsh-my-observability-detail-key' }, key), createElement('span', { className: 'dsh-my-observability-detail-value' }, String(value))));
966
+ }
967
+ }
968
+ return rows.length > 0 ? rows : null;
885
969
  }
886
-
887
970
  /** 单条事件行:节点圆点 + 类型图标 + 徽标/时间 + 摘要(hover/active 反馈)。
888
971
  * 摘要命中关键词时以 mark 高亮;插件事件可点击展开详情(原因/参数)。 */
889
972
  function EventRow({ event, keyword }) {
890
- const meta = eventMeta(event)
891
- const kind = typeKind(event)
892
- const detail = eventDetail(event)
893
- const [open, setOpen] = useState(false)
894
- return createElement(
895
- '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
- },
902
- createElement('span', { className: `dsh-my-observability-node dsh-my-observability-node-${kind}` }),
903
- createElement(
904
- 'span',
905
- { className: `dsh-my-observability-event-icon dsh-my-observability-icon-${kind}` },
906
- typeIcon(event),
907
- ),
908
- createElement(
909
- 'span',
910
- { className: 'dsh-my-observability-event-body' },
911
- createElement(
912
- 'span',
913
- { className: 'dsh-my-observability-event-head' },
914
- createElement(
915
- 'span',
916
- { className: `dsh-my-observability-badge dsh-my-observability-badge-${kind}` },
917
- typeLabel(event),
918
- ),
919
- createElement('span', { className: 'dsh-my-observability-time' }, timeText(event.time)),
920
- ),
921
- meta !== ''
922
- ? createElement(
923
- 'span',
924
- { className: 'dsh-my-observability-event-meta' },
925
- createElement(HighlightText, { text: meta, keyword }),
926
- )
927
- : null,
928
- detail !== null && open ? createElement('div', { className: 'dsh-my-observability-event-detail' }, detail) : null,
929
- ),
930
- )
973
+ const meta = eventMeta(event);
974
+ const kind = typeKind(event);
975
+ const detail = eventDetail(event);
976
+ const [open, setOpen] = useState(false);
977
+ return createElement('button', {
978
+ className: 'dsh-my-observability-event',
979
+ type: 'button',
980
+ 'aria-expanded': detail !== null ? open : undefined,
981
+ onClick: detail !== null ? () => setOpen(!open) : undefined,
982
+ }, createElement('span', { className: `dsh-my-observability-node dsh-my-observability-node-${kind}` }), createElement('span', { className: `dsh-my-observability-event-icon dsh-my-observability-icon-${kind}` }, typeIcon(event)), createElement('span', { className: 'dsh-my-observability-event-body' }, createElement('span', { className: 'dsh-my-observability-event-head' }, createElement('span', { className: `dsh-my-observability-badge dsh-my-observability-badge-${kind}` }, typeLabel(event)), createElement('span', { className: 'dsh-my-observability-time' }, timeText(event.time))), meta !== ''
983
+ ? createElement('span', { className: 'dsh-my-observability-event-meta' }, createElement(HighlightText, { text: meta, keyword }))
984
+ : null, detail !== null && open ? createElement('div', { className: 'dsh-my-observability-event-detail' }, detail) : null));
931
985
  }
932
-
933
986
  /** 类型过滤按钮组(aria-pressed 选中态;plugin 过滤插件事件,issue #154)。 */
934
987
  function TypeFilter({ filter, onFilter }) {
935
- const options = [
936
- ['', strings.filterAll()],
937
- ['agent_status', strings.filterStatus()],
938
- ['llm_stream', strings.filterLlm()],
939
- ['tool', strings.filterTools()],
940
- ['plugin', strings.filterPlugin()],
941
- ]
942
- return createElement(
943
- 'div',
944
- { className: 'dsh-my-observability-filters' },
945
- options.map(([value, label]) =>
946
- createElement(
947
- 'button',
948
- {
949
- key: value,
950
- type: 'button',
951
- className: `dsh-my-observability-chip${filter === value ? ' dsh-my-observability-chip-active' : ''}`,
952
- 'aria-pressed': filter === value,
953
- onClick: () => onFilter(value),
954
- },
955
- label,
956
- ),
957
- ),
958
- )
988
+ const options = [
989
+ ['', strings.filterAll()],
990
+ ['agent_status', strings.filterStatus()],
991
+ ['llm_stream', strings.filterLlm()],
992
+ ['tool', strings.filterTools()],
993
+ ['plugin', strings.filterPlugin()],
994
+ ];
995
+ return createElement('div', { className: 'dsh-my-observability-filters' }, options.map(([value, label]) => createElement('button', {
996
+ key: value,
997
+ type: 'button',
998
+ className: `dsh-my-observability-chip${filter === value ? ' dsh-my-observability-chip-active' : ''}`,
999
+ 'aria-pressed': filter === value,
1000
+ onClick: () => onFilter(value),
1001
+ }, label)));
959
1002
  }
960
-
961
1003
  /** 拉取会话列表与事件(选中为空时自动选当前/首个会话)。 */
962
1004
  async function loadReplayData(selected, currentSession, setters) {
963
- try {
964
- const list = await apiJson('/observability/api/sessions')
965
- setters.setSessions(list)
966
- if (selected === '' && list.length > 0) {
967
- const preferred = list.some((s) => s.sessionId === currentSession) ? currentSession : list[0].sessionId
968
- setters.setSelected(preferred)
969
- return
1005
+ try {
1006
+ const list = await apiJson('/observability/api/sessions');
1007
+ setters.setSessions(list);
1008
+ if (selected === '' && list.length > 0) {
1009
+ const preferred = list.some((s) => s.sessionId === currentSession) ? currentSession : list[0].sessionId;
1010
+ setters.setSelected(preferred);
1011
+ return;
1012
+ }
1013
+ const query = selected !== ''
1014
+ ? `/observability/api/events?sessionId=${encodeURIComponent(selected)}&limit=300`
1015
+ : '/observability/api/events?limit=0';
1016
+ setters.setEvents(await apiJson(query));
1017
+ setters.setError('');
1018
+ }
1019
+ catch (err) {
1020
+ setters.setError(err instanceof Error ? err.message : String(err));
1021
+ }
1022
+ finally {
1023
+ setters.setLoading(false);
970
1024
  }
971
- const query =
972
- selected !== ''
973
- ? `/observability/api/events?sessionId=${encodeURIComponent(selected)}&limit=300`
974
- : '/observability/api/events?limit=0'
975
- setters.setEvents(await apiJson(query))
976
- setters.setError('')
977
- } catch (err) {
978
- setters.setError(err instanceof Error ? err.message : String(err))
979
- } finally {
980
- setters.setLoading(false)
981
- }
982
1025
  }
983
-
984
1026
  /** 下拉选项文案:可读标题(首条用户消息)优先,无标题回退 UUID 短显;
985
1027
  * 附加事件数与时间,用户一眼看出"哪个对话"。 */
986
1028
  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)}`
1029
+ const title = typeof s.title === 'string' && s.title !== '' ? s.title : strings.sessionFallback(shortId(s.sessionId));
1030
+ return `${title} · ${strings.eventCount(s.count)}`;
989
1031
  }
990
-
991
1032
  /** 会话 id 短显示(UUID 取前 8 位)。 */
992
1033
  function shortId(sessionId) {
993
- return typeof sessionId === 'string' && sessionId.length > 8 ? `${sessionId.slice(0, 8)}…` : sessionId || ''
1034
+ return typeof sessionId === 'string' && sessionId.length > 8 ? `${sessionId.slice(0, 8)}…` : sessionId || '';
994
1035
  }
995
-
996
1036
  /** 工具栏:会话选择 + 手动刷新 + 类型过滤。 */
997
- function ReplayToolbar({ sessions, selected, onSelect, filter, onFilter, onRefresh }) {
998
- return createElement(
999
- 'div',
1000
- { className: 'dsh-my-observability-toolbar' },
1001
- createElement(
1002
- 'div',
1003
- { className: 'dsh-my-observability-toolbar-row' },
1004
- createElement(
1005
- 'select',
1006
- {
1007
- className: 'dsh-my-observability-select',
1008
- value: selected,
1009
- disabled: sessions.length === 0,
1010
- onChange: (e) => onSelect(e.target.value),
1011
- },
1012
- sessions.length === 0
1013
- ? createElement('option', { value: '' }, strings.allSessions())
1014
- : sessions.map((s) =>
1015
- createElement('option', { key: s.sessionId, value: s.sessionId }, sessionOptionLabel(s)),
1016
- ),
1017
- ),
1018
- createElement(
1019
- 'button',
1020
- {
1021
- type: 'button',
1022
- className: 'dsh-my-observability-iconbtn',
1023
- 'aria-label': strings.refresh(),
1024
- title: strings.refresh(),
1025
- onClick: onRefresh,
1026
- },
1027
- icon.refresh(15),
1028
- ),
1029
- ),
1030
- createElement(TypeFilter, { filter, onFilter }),
1031
- )
1037
+ function ReplayToolbar({ sessions, selected, onSelect, filter, onFilter, onRefresh, }) {
1038
+ return createElement('div', { className: 'dsh-my-observability-toolbar' }, createElement('div', { className: 'dsh-my-observability-toolbar-row' }, createElement('select', {
1039
+ className: 'dsh-my-observability-select',
1040
+ value: selected,
1041
+ disabled: sessions.length === 0,
1042
+ onChange: (e) => onSelect(e.target.value),
1043
+ }, sessions.length === 0
1044
+ ? createElement('option', { value: '' }, strings.allSessions())
1045
+ : sessions.map((s) => createElement('option', { key: s.sessionId, value: s.sessionId }, sessionOptionLabel(s)))), createElement('button', {
1046
+ type: 'button',
1047
+ className: 'dsh-my-observability-iconbtn',
1048
+ 'aria-label': strings.refresh(),
1049
+ title: strings.refresh(),
1050
+ onClick: onRefresh,
1051
+ }, icon.refresh(15))), createElement(TypeFilter, { filter, onFilter }));
1032
1052
  }
1033
-
1034
1053
  /** 加载中状态(旋转刷新图标 + 次级色文案,不阻塞布局)。 */
1035
1054
  function LoadingState() {
1036
- return createElement(
1037
- 'div',
1038
- { className: 'dsh-my-observability-state' },
1039
- icon.refresh(14),
1040
- createElement('span', null, strings.loading()),
1041
- )
1055
+ return createElement('div', { className: 'dsh-my-observability-state' }, icon.refresh(14), createElement('span', null, strings.loading()));
1042
1056
  }
1043
-
1044
1057
  /** 空状态(图标 + 主文案 + hint 两行结构)。 */
1045
1058
  function EmptyState() {
1046
- return createElement(
1047
- 'div',
1048
- { className: 'dsh-my-observability-empty' },
1049
- createElement('span', { className: 'dsh-my-observability-empty-icon' }, icon.clock(20)),
1050
- createElement('span', null, strings.emptyEvents()),
1051
- createElement('span', { className: 'dsh-my-observability-empty-hint' }, strings.emptyEventsHint()),
1052
- )
1059
+ return createElement('div', { className: 'dsh-my-observability-empty' }, createElement('span', { className: 'dsh-my-observability-empty-icon' }, icon.clock(20)), createElement('span', null, strings.emptyEvents()), createElement('span', { className: 'dsh-my-observability-empty-hint' }, strings.emptyEventsHint()));
1053
1060
  }
1054
-
1055
1061
  /** 错误状态(错误色文案 + 重试按钮)。 */
1056
1062
  function ErrorState({ message, onRetry }) {
1057
- return createElement(
1058
- 'div',
1059
- { className: 'dsh-my-observability-error' },
1060
- createElement('span', { className: 'dsh-my-observability-error-text' }, `${strings.loadError()}:${message}`),
1061
- createElement(
1062
- 'button',
1063
- {
1063
+ return createElement('div', { className: 'dsh-my-observability-error' }, createElement('span', { className: 'dsh-my-observability-error-text' }, `${strings.loadError()}:${message}`), createElement('button', {
1064
1064
  type: 'button',
1065
1065
  className: 'dsh-my-observability-iconbtn',
1066
1066
  'aria-label': strings.retry(),
1067
1067
  title: strings.retry(),
1068
1068
  onClick: onRetry,
1069
- },
1070
- icon.refresh(15),
1071
- ),
1072
- )
1069
+ }, icon.refresh(15)));
1073
1070
  }
1074
-
1075
1071
  /** 轨迹回放主面板:会话选择 + 类型过滤 + 搜索/组合过滤 + 导出 + 统计 + 时间轴。
1076
1072
  * 状态与派生值集中在 replay-ext.js 的 useReplayState;本组件只拼装视图。
1077
1073
  * 面板可见时轮询、隐藏时暂停。 */
1078
1074
  function ReplayPanel(props) {
1079
- const s = useReplayState(props)
1080
- return createElement(
1081
- 'div',
1082
- { className: 'dsh-my-observability-panel' },
1083
- createElement(ResourcePanel, { resource: s.resource }),
1084
- createElement(ReplayToolbar, {
1085
- sessions: s.sessions,
1086
- selected: s.selected,
1087
- onSelect: s.setSelected,
1088
- filter: s.filter,
1089
- onFilter: s.setFilter,
1090
- onRefresh: s.retry,
1091
- }),
1092
- createElement(SearchFilterBar, {
1093
- keyword: s.keyword,
1094
- onKeyword: s.setKeyword,
1095
- timeStart: s.timeStart,
1096
- onTimeStart: s.setTimeStart,
1097
- timeEnd: s.timeEnd,
1098
- onTimeEnd: s.setTimeEnd,
1099
- result: s.result,
1100
- onResult: s.setResult,
1101
- onClear: s.clearFilters,
1102
- }),
1103
- createElement(ExportBar, {
1104
- scope: s.scope,
1105
- onScope: s.setScope,
1106
- onExportJson: () => void s.onExport('json'),
1107
- onExportCsv: () => void s.onExport('csv'),
1108
- showStats: s.showStats,
1109
- onToggleStats: () => s.setShowStats((value) => !value),
1110
- disabled: !s.canExport,
1111
- }),
1112
- s.error !== '' ? createElement(ErrorState, { message: s.error, onRetry: s.retry }) : null,
1113
- s.loading && s.error === '' ? createElement(LoadingState, null) : null,
1114
- !s.loading && s.error === '' && s.filtered.length === 0
1115
- ? s.hasFilter
1116
- ? createElement(NoMatchesState, null)
1117
- : createElement(EmptyState, null)
1118
- : null,
1119
- s.showStats ? createElement(StatsPanel, { events: s.filtered }) : null,
1120
- createElement('div', { className: 'dsh-my-observability-timeline' }, s.rows),
1121
- )
1122
- }
1123
-
1124
- // ── 资源监控区块(写放大/资源超限预警,见 lib/resource-monitor.js)──────
1075
+ const s = useReplayState(props);
1076
+ return createElement('div', { className: 'dsh-my-observability-panel' }, createElement(ResourcePanel, { resource: s.resource }), createElement(ReplayToolbar, {
1077
+ sessions: s.sessions,
1078
+ selected: s.selected,
1079
+ onSelect: s.setSelected,
1080
+ filter: s.filter,
1081
+ onFilter: s.setFilter,
1082
+ onRefresh: s.retry,
1083
+ }), createElement(SearchFilterBar, {
1084
+ keyword: s.keyword,
1085
+ onKeyword: s.setKeyword,
1086
+ timeStart: s.timeStart,
1087
+ onTimeStart: s.setTimeStart,
1088
+ timeEnd: s.timeEnd,
1089
+ onTimeEnd: s.setTimeEnd,
1090
+ result: s.result,
1091
+ onResult: s.setResult,
1092
+ onClear: s.clearFilters,
1093
+ }), createElement(ExportBar, {
1094
+ scope: s.scope,
1095
+ onScope: s.setScope,
1096
+ onExportJson: () => void s.onExport('json'),
1097
+ onExportCsv: () => void s.onExport('csv'),
1098
+ showStats: s.showStats,
1099
+ onToggleStats: () => s.setShowStats((value) => !value),
1100
+ disabled: !s.canExport,
1101
+ }), s.error !== '' ? createElement(ErrorState, { message: s.error, onRetry: s.retry }) : null, s.loading && s.error === '' ? createElement(LoadingState, null) : null, !s.loading && s.error === '' && s.filtered.length === 0
1102
+ ? s.hasFilter
1103
+ ? createElement(NoMatchesState, null)
1104
+ : createElement(EmptyState, null)
1105
+ : null, s.showStats ? createElement(StatsPanel, { events: s.filtered }) : null, createElement('div', { className: 'dsh-my-observability-timeline' }, s.rows));
1106
+ }
1107
+
1108
+ "use strict";
1109
+ // ── 资源监控区块(写放大/资源超限预警,见 lib/resource-monitor.js)──────
1125
1110
  // 依赖 replay.js 先拼接(apiJson)与 i18n.js(strings)。纯函数声明文本。
1126
-
1127
- const RESOURCE_POLL_MS = 15000
1128
-
1111
+ const RESOURCE_POLL_MS = 15000;
1129
1112
  function fmtResourceBytes(bytes) {
1130
- if (!Number.isFinite(bytes)) return '-'
1131
- return `${(bytes / 1048576).toFixed(1)} MB`
1113
+ if (!Number.isFinite(bytes))
1114
+ return '-';
1115
+ return `${(bytes / 1048576).toFixed(1)} MB`;
1132
1116
  }
1133
-
1134
1117
  /** 资源采样状态:可见时每 15s 轮询 /observability/api/resources。 */
1135
1118
  function useResourceState(visible) {
1136
- const [resource, setResource] = useState(null)
1137
- useEffect(() => {
1138
- if (!visible) return undefined
1139
- let alive = true
1140
- const tick = () => {
1141
- if (alive)
1142
- apiJson('/observability/api/resources')
1143
- .then(setResource)
1144
- .catch(() => {})
1145
- }
1146
- tick()
1147
- const timer = setInterval(tick, RESOURCE_POLL_MS)
1148
- return () => {
1149
- alive = false
1150
- clearInterval(timer)
1151
- }
1152
- }, [visible])
1153
- return resource
1119
+ const [resource, setResource] = useState(null);
1120
+ useEffect(() => {
1121
+ if (!visible)
1122
+ return undefined;
1123
+ let alive = true;
1124
+ const tick = () => {
1125
+ if (alive)
1126
+ apiJson('/observability/api/resources')
1127
+ .then(setResource)
1128
+ .catch(() => { });
1129
+ };
1130
+ tick();
1131
+ const timer = setInterval(tick, RESOURCE_POLL_MS);
1132
+ return () => {
1133
+ alive = false;
1134
+ clearInterval(timer);
1135
+ };
1136
+ }, [visible]);
1137
+ return resource;
1154
1138
  }
1155
-
1156
1139
  function ResourceMetric({ label, value }) {
1157
- return createElement(
1158
- 'div',
1159
- { className: 'dsh-my-observability-resource-metric' },
1160
- createElement('span', { className: 'dsh-my-observability-resource-label' }, label),
1161
- createElement('span', { className: 'dsh-my-observability-resource-value' }, value),
1162
- )
1140
+ return createElement('div', { className: 'dsh-my-observability-resource-metric' }, createElement('span', { className: 'dsh-my-observability-resource-label' }, label), createElement('span', { className: 'dsh-my-observability-resource-value' }, value));
1163
1141
  }
1164
-
1165
1142
  /** 资源面板:四指标 + 告警列表(write-rate/file-size level=error 红色,cpu/memory warn 黄色)。 */
1166
1143
  function ResourcePanel({ resource }) {
1167
- if (resource === null || resource === undefined) {
1168
- return createElement('div', { className: 'dsh-my-observability-resource' }, strings.resourceLoading())
1169
- }
1170
- const alerts = Array.isArray(resource.alerts) ? resource.alerts : []
1171
- return createElement(
1172
- 'div',
1173
- { className: 'dsh-my-observability-resource' },
1174
- createElement('div', { className: 'dsh-my-observability-resource-head' }, strings.resourceTitle()),
1175
- createElement(
1176
- 'div',
1177
- { className: 'dsh-my-observability-resource-grid' },
1178
- createElement(ResourceMetric, { label: strings.resourceFile(), value: fmtResourceBytes(resource.fileBytes) }),
1179
- createElement(ResourceMetric, {
1144
+ if (resource === null || resource === undefined) {
1145
+ return createElement('div', { className: 'dsh-my-observability-resource' }, strings.resourceLoading());
1146
+ }
1147
+ const alerts = Array.isArray(resource.alerts) ? resource.alerts : [];
1148
+ return createElement('div', { className: 'dsh-my-observability-resource' }, createElement('div', { className: 'dsh-my-observability-resource-head' }, strings.resourceTitle()), createElement('div', { className: 'dsh-my-observability-resource-grid' }, createElement(ResourceMetric, { label: strings.resourceFile(), value: fmtResourceBytes(resource.fileBytes) }), createElement(ResourceMetric, {
1180
1149
  label: strings.resourceRate(),
1181
1150
  value: `${fmtResourceBytes(resource.writeRateBytesPerHour)}/h`,
1182
- }),
1183
- createElement(ResourceMetric, {
1151
+ }), createElement(ResourceMetric, {
1184
1152
  label: strings.resourceCpu(),
1185
1153
  value: `${Math.round(resource.cpuPercent ?? 0)}%`,
1186
- }),
1187
- createElement(ResourceMetric, { label: strings.resourceMem(), value: fmtResourceBytes(resource.memoryBytes) }),
1188
- ),
1189
- alerts.length > 0
1190
- ? createElement(
1191
- 'div',
1192
- { className: 'dsh-my-observability-resource-alerts' },
1193
- alerts.map((alert) =>
1194
- createElement(
1195
- 'div',
1196
- { className: `dsh-my-observability-resource-alert dsh-my-observability-resource-alert-${alert.level}` },
1197
- alert.message,
1198
- ),
1199
- ),
1200
- )
1201
- : null,
1202
- )
1154
+ }), createElement(ResourceMetric, { label: strings.resourceMem(), value: fmtResourceBytes(resource.memoryBytes) })), alerts.length > 0
1155
+ ? createElement('div', { className: 'dsh-my-observability-resource-alerts' }, alerts.map((alert) => createElement('div', { className: `dsh-my-observability-resource-alert dsh-my-observability-resource-alert-${alert.level}` }, alert.message)))
1156
+ : null);
1203
1157
  }
1204
1158
 
1205
- // ── 审计视图扩展:搜索 / 组合过滤 / 导出 / 统计 / 高亮(replay.js 拆出)────
1159
+ "use strict";
1160
+ // ── 审计视图扩展:搜索 / 组合过滤 / 导出 / 统计 / 高亮(replay.js 拆出)────
1206
1161
  // 依赖 replay.js(REPLAY_POLL_MS 等常量与 loadReplayData/EventRow)与
1207
1162
  // audit-view.js 纯函数(applyAuditFilter 等)。始终以 function 声明提升。
1208
-
1209
- const REPLAY_POLL_MS = 5000
1210
- const ALL_SESSIONS = '*'
1211
- const EXPORT_LIMIT_ALL = 20000
1212
-
1163
+ const REPLAY_POLL_MS = 5000;
1164
+ const ALL_SESSIONS = '*';
1165
+ const EXPORT_LIMIT_ALL = 20000;
1213
1166
  /** `datetime-local` 值 → 毫秒时间戳(空/非法返回 undefined)。 */
1214
1167
  function datetimeToMs(value) {
1215
- if (typeof value !== 'string' || value.trim() === '') return undefined
1216
- const ms = new Date(value).getTime()
1217
- return Number.isFinite(ms) ? ms : undefined
1168
+ if (typeof value !== 'string' || value.trim() === '')
1169
+ return undefined;
1170
+ const ms = new Date(value).getTime();
1171
+ return Number.isFinite(ms) ? ms : undefined;
1218
1172
  }
1219
-
1220
1173
  /** 是否有活动过滤条件(决定无匹配/空状态展示)。 */
1221
1174
  function hasActiveFilter(criteria) {
1222
- return (
1223
- criteria.keyword !== '' ||
1224
- criteria.result !== '' ||
1225
- criteria.timeStart !== undefined ||
1226
- criteria.timeEnd !== undefined ||
1227
- criteria.type !== ''
1228
- )
1175
+ return (criteria.keyword !== '' ||
1176
+ criteria.result !== '' ||
1177
+ criteria.timeStart !== undefined ||
1178
+ criteria.timeEnd !== undefined ||
1179
+ criteria.type !== '');
1229
1180
  }
1230
-
1231
1181
  /** 命中关键词高亮(`mark` 包裹命中段;无关键词时原样文本)。 */
1232
1182
  function HighlightText({ text, keyword }) {
1233
- const segments = highlightSegments(text, keyword)
1234
- return createElement(
1235
- 'span',
1236
- null,
1237
- segments.map((seg, index) =>
1238
- seg.hit ? createElement('mark', { key: index, className: 'dsh-my-observability-mark' }, seg.text) : seg.text,
1239
- ),
1240
- )
1183
+ const segments = highlightSegments(text, keyword);
1184
+ return createElement('span', null, segments.map((seg, index) => seg.hit ? createElement('mark', { key: index, className: 'dsh-my-observability-mark' }, seg.text) : seg.text));
1241
1185
  }
1242
-
1243
1186
  /** 搜索 / 组合过滤栏:关键词 + 时间范围 + 成功/失败。 */
1244
1187
  /** 搜索框行(含清除按钮)。 */
1245
- function SearchInput({ keyword, onKeyword, onClear }) {
1246
- return createElement(
1247
- 'div',
1248
- { className: 'dsh-my-observability-search-row' },
1249
- createElement('input', {
1250
- className: 'dsh-my-observability-input',
1251
- type: 'search',
1252
- value: keyword,
1253
- placeholder: strings.searchPlaceholder(),
1254
- onChange: (e) => onKeyword(e.target.value),
1255
- }),
1256
- keyword !== ''
1257
- ? createElement(
1258
- 'button',
1259
- {
1188
+ function SearchInput({ keyword, onKeyword, onClear, }) {
1189
+ return createElement('div', { className: 'dsh-my-observability-search-row' }, createElement('input', {
1190
+ className: 'dsh-my-observability-input',
1191
+ type: 'search',
1192
+ value: keyword,
1193
+ placeholder: strings.searchPlaceholder(),
1194
+ onChange: (e) => onKeyword(e.target.value),
1195
+ }), keyword !== ''
1196
+ ? createElement('button', {
1260
1197
  type: 'button',
1261
1198
  className: 'dsh-my-observability-iconbtn',
1262
1199
  'aria-label': strings.clearFilters(),
1263
1200
  title: strings.clearFilters(),
1264
1201
  onClick: onClear,
1265
- },
1266
- icon.close(15),
1267
- )
1268
- : null,
1269
- )
1202
+ }, icon.close(15))
1203
+ : null);
1270
1204
  }
1271
-
1272
1205
  /** 时间范围行(开始/结束,datetime-local)。 */
1273
- function TimeRangeInput({ timeStart, onTimeStart, timeEnd, onTimeEnd }) {
1274
- return createElement(
1275
- 'div',
1276
- { className: 'dsh-my-observability-time-row' },
1277
- createElement('label', { className: 'dsh-my-observability-time-label' }, strings.timeStartLabel()),
1278
- createElement('input', {
1279
- className: 'dsh-my-observability-input dsh-my-observability-time-input',
1280
- type: 'datetime-local',
1281
- value: timeStart,
1282
- onChange: (e) => onTimeStart(e.target.value),
1283
- }),
1284
- createElement('label', { className: 'dsh-my-observability-time-label' }, strings.timeEndLabel()),
1285
- createElement('input', {
1286
- className: 'dsh-my-observability-input dsh-my-observability-time-input',
1287
- type: 'datetime-local',
1288
- value: timeEnd,
1289
- onChange: (e) => onTimeEnd(e.target.value),
1290
- }),
1291
- )
1206
+ function TimeRangeInput({ timeStart, onTimeStart, timeEnd, onTimeEnd, }) {
1207
+ return createElement('div', { className: 'dsh-my-observability-time-row' }, createElement('label', { className: 'dsh-my-observability-time-label' }, strings.timeStartLabel()), createElement('input', {
1208
+ className: 'dsh-my-observability-input dsh-my-observability-time-input',
1209
+ type: 'datetime-local',
1210
+ value: timeStart,
1211
+ onChange: (e) => onTimeStart(e.target.value),
1212
+ }), createElement('label', { className: 'dsh-my-observability-time-label' }, strings.timeEndLabel()), createElement('input', {
1213
+ className: 'dsh-my-observability-input dsh-my-observability-time-input',
1214
+ type: 'datetime-local',
1215
+ value: timeEnd,
1216
+ onChange: (e) => onTimeEnd(e.target.value),
1217
+ }));
1292
1218
  }
1293
-
1294
1219
  /** 成功/失败结果过滤组。 */
1295
1220
  function ResultFilter({ result, onResult }) {
1296
- const options = [
1297
- ['', strings.filterAllResult()],
1298
- ['success', strings.filterSuccess()],
1299
- ['fail', strings.filterFail()],
1300
- ]
1301
- return createElement(
1302
- 'div',
1303
- { className: 'dsh-my-observability-filters' },
1304
- options.map(([value, label]) =>
1305
- createElement(
1306
- 'button',
1307
- {
1308
- key: value,
1309
- type: 'button',
1310
- className: `dsh-my-observability-chip${result === value ? ' dsh-my-observability-chip-active' : ''}`,
1311
- 'aria-pressed': result === value,
1312
- onClick: () => onResult(value),
1313
- },
1314
- label,
1315
- ),
1316
- ),
1317
- )
1221
+ const options = [
1222
+ ['', strings.filterAllResult()],
1223
+ ['success', strings.filterSuccess()],
1224
+ ['fail', strings.filterFail()],
1225
+ ];
1226
+ return createElement('div', { className: 'dsh-my-observability-filters' }, options.map(([value, label]) => createElement('button', {
1227
+ key: value,
1228
+ type: 'button',
1229
+ className: `dsh-my-observability-chip${result === value ? ' dsh-my-observability-chip-active' : ''}`,
1230
+ 'aria-pressed': result === value,
1231
+ onClick: () => onResult(value),
1232
+ }, label)));
1318
1233
  }
1319
-
1320
1234
  /** 搜索 / 组合过滤栏:关键词 + 时间范围 + 成功/失败。 */
1321
- function SearchFilterBar({
1322
- keyword,
1323
- onKeyword,
1324
- timeStart,
1325
- onTimeStart,
1326
- timeEnd,
1327
- onTimeEnd,
1328
- result,
1329
- onResult,
1330
- onClear,
1331
- }) {
1332
- return createElement(
1333
- 'div',
1334
- { className: 'dsh-my-observability-toolbar' },
1335
- createElement(SearchInput, { keyword, onKeyword, onClear }),
1336
- createElement(TimeRangeInput, { timeStart, onTimeStart, timeEnd, onTimeEnd }),
1337
- createElement(ResultFilter, { result, onResult }),
1338
- )
1235
+ function SearchFilterBar({ keyword, onKeyword, timeStart, onTimeStart, timeEnd, onTimeEnd, result, onResult, onClear, }) {
1236
+ return createElement('div', { className: 'dsh-my-observability-toolbar' }, createElement(SearchInput, { keyword, onKeyword, onClear }), createElement(TimeRangeInput, { timeStart, onTimeStart, timeEnd, onTimeEnd }), createElement(ResultFilter, { result, onResult }));
1339
1237
  }
1340
-
1341
1238
  /** 导出栏:导出范围选择(当前会话/全部会话)+ JSON/CSV 按钮 + 统计开关。 */
1342
- function ExportBar({ scope, onScope, onExportJson, onExportCsv, showStats, onToggleStats, disabled }) {
1343
- return createElement(
1344
- 'div',
1345
- { className: 'dsh-my-observability-export' },
1346
- createElement(
1347
- 'div',
1348
- { className: 'dsh-my-observability-toolbar-row' },
1349
- createElement(
1350
- 'select',
1351
- {
1352
- className: 'dsh-my-observability-select',
1353
- value: scope,
1354
- disabled,
1355
- onChange: (e) => onScope(e.target.value),
1356
- },
1357
- createElement('option', { value: 'session' }, strings.scopeSession()),
1358
- createElement('option', { value: 'all' }, strings.scopeAll()),
1359
- ),
1360
- createElement(
1361
- 'button',
1362
- { type: 'button', className: 'dsh-my-observability-btn', disabled, onClick: onExportJson },
1363
- strings.exportJson(),
1364
- ),
1365
- createElement(
1366
- 'button',
1367
- { type: 'button', className: 'dsh-my-observability-btn', disabled, onClick: onExportCsv },
1368
- strings.exportCsv(),
1369
- ),
1370
- createElement(
1371
- 'button',
1372
- {
1373
- type: 'button',
1374
- className: 'dsh-my-observability-btn',
1375
- 'aria-pressed': showStats,
1376
- onClick: onToggleStats,
1377
- },
1378
- strings.statsTitle(),
1379
- ),
1380
- ),
1381
- )
1239
+ function ExportBar({ scope, onScope, onExportJson, onExportCsv, showStats, onToggleStats, disabled, }) {
1240
+ return createElement('div', { className: 'dsh-my-observability-export' }, createElement('div', { className: 'dsh-my-observability-toolbar-row' }, createElement('select', {
1241
+ className: 'dsh-my-observability-select',
1242
+ value: scope,
1243
+ disabled,
1244
+ onChange: (e) => onScope(e.target.value),
1245
+ }, createElement('option', { value: 'session' }, strings.scopeSession()), createElement('option', { value: 'all' }, strings.scopeAll())), createElement('button', { type: 'button', className: 'dsh-my-observability-btn', disabled, onClick: onExportJson }, strings.exportJson()), createElement('button', { type: 'button', className: 'dsh-my-observability-btn', disabled, onClick: onExportCsv }, strings.exportCsv()), createElement('button', {
1246
+ type: 'button',
1247
+ className: 'dsh-my-observability-btn',
1248
+ 'aria-pressed': showStats,
1249
+ onClick: onToggleStats,
1250
+ }, strings.statsTitle())));
1382
1251
  }
1383
-
1384
1252
  /** 工具调用统计视图(Top N 调用次数 + 失败率)。 */
1385
1253
  function StatsPanel({ events }) {
1386
- const stats = computeToolStats(events, 5)
1387
- if (stats.length === 0)
1388
- return createElement('div', { className: 'dsh-my-observability-stats-empty' }, strings.statsEmpty())
1389
- return createElement(
1390
- 'div',
1391
- { className: 'dsh-my-observability-stats' },
1392
- createElement('div', { className: 'dsh-my-observability-stats-title' }, strings.statsTitle()),
1393
- createElement(
1394
- 'table',
1395
- { className: 'dsh-my-observability-stats-table' },
1396
- createElement(
1397
- 'thead',
1398
- null,
1399
- createElement(
1400
- 'tr',
1401
- null,
1402
- createElement('th', null, strings.statsTool()),
1403
- createElement('th', null, strings.statsCalls()),
1404
- createElement('th', null, strings.statsFailRate()),
1405
- ),
1406
- ),
1407
- createElement(
1408
- 'tbody',
1409
- null,
1410
- stats.map((s) =>
1411
- createElement(
1412
- 'tr',
1413
- { key: s.tool },
1414
- createElement('td', null, s.tool),
1415
- createElement('td', null, String(s.calls)),
1416
- createElement('td', null, `${(s.failRate * 100).toFixed(1)}%`),
1417
- ),
1418
- ),
1419
- ),
1420
- ),
1421
- )
1254
+ const stats = computeToolStats(events, 5);
1255
+ if (stats.length === 0)
1256
+ return createElement('div', { className: 'dsh-my-observability-stats-empty' }, strings.statsEmpty());
1257
+ return createElement('div', { className: 'dsh-my-observability-stats' }, createElement('div', { className: 'dsh-my-observability-stats-title' }, strings.statsTitle()), createElement('table', { className: 'dsh-my-observability-stats-table' }, createElement('thead', null, createElement('tr', null, createElement('th', null, strings.statsTool()), createElement('th', null, strings.statsCalls()), createElement('th', null, strings.statsFailRate()))), createElement('tbody', null, stats.map((s) => createElement('tr', { key: s.tool }, createElement('td', null, s.tool), createElement('td', null, String(s.calls)), createElement('td', null, `${(s.failRate * 100).toFixed(1)}%`))))));
1422
1258
  }
1423
-
1424
1259
  /** 无匹配状态(搜索/过滤条件命中 0 条)。 */
1425
1260
  function NoMatchesState() {
1426
- return createElement(
1427
- 'div',
1428
- { className: 'dsh-my-observability-empty' },
1429
- createElement('span', { className: 'dsh-my-observability-empty-icon' }, icon.search(20)),
1430
- createElement('span', null, strings.noMatches()),
1431
- )
1261
+ return createElement('div', { className: 'dsh-my-observability-empty' }, createElement('span', { className: 'dsh-my-observability-empty-icon' }, icon.search(20)), createElement('span', null, strings.noMatches()));
1432
1262
  }
1433
-
1434
1263
  /** 触发浏览器下载(Blob + a[download])。 */
1435
1264
  function downloadAudit(content, format) {
1436
- const stamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')
1437
- const filename = `${strings.exportFileName()}-${stamp}.${format}`
1438
- const mime = format === 'json' ? 'application/json' : 'text/csv;charset=utf-8'
1439
- const blob = new Blob([content], { type: mime })
1440
- const url = URL.createObjectURL(blob)
1441
- const a = document.createElement('a')
1442
- a.href = url
1443
- a.download = filename
1444
- document.body.appendChild(a)
1445
- a.click()
1446
- a.remove()
1447
- URL.revokeObjectURL(url)
1265
+ const stamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
1266
+ const filename = `${strings.exportFileName()}-${stamp}.${format}`;
1267
+ const mime = format === 'json' ? 'application/json' : 'text/csv;charset=utf-8';
1268
+ const blob = new Blob([content], { type: mime });
1269
+ const url = URL.createObjectURL(blob);
1270
+ const a = document.createElement('a');
1271
+ a.href = url;
1272
+ a.download = filename;
1273
+ document.body.appendChild(a);
1274
+ a.click();
1275
+ a.remove();
1276
+ URL.revokeObjectURL(url);
1448
1277
  }
1449
-
1450
1278
  /** 拉取全部会话事件(导出范围「全部会话」用),并应用当前过滤条件。 */
1451
1279
  async function allSessionEvents(criteria) {
1452
- const all = await apiJson(
1453
- `/observability/api/events?sessionId=${encodeURIComponent(ALL_SESSIONS)}&limit=${EXPORT_LIMIT_ALL}`,
1454
- )
1455
- return applyAuditFilter(all, criteria)
1280
+ const all = await apiJson(`/observability/api/events?sessionId=${encodeURIComponent(ALL_SESSIONS)}&limit=${EXPORT_LIMIT_ALL}`);
1281
+ return applyAuditFilter(all, criteria);
1456
1282
  }
1457
-
1458
1283
  /** 导出:依据范围(当前会话=过滤后结果 / 全部会话=拉取后过滤)生成并下载。 */
1459
1284
  async function runExport(format, scope, filtered, criteria, onError) {
1460
- try {
1461
- const dataEvents = scope === 'session' ? filtered : await allSessionEvents(criteria)
1462
- downloadAudit(format === 'json' ? auditToJson(dataEvents) : auditToCsv(dataEvents), format)
1463
- } catch (err) {
1464
- onError(err instanceof Error ? err.message : String(err))
1465
- }
1285
+ try {
1286
+ const dataEvents = scope === 'session' ? filtered : await allSessionEvents(criteria);
1287
+ downloadAudit(format === 'json' ? auditToJson(dataEvents) : auditToCsv(dataEvents), format);
1288
+ }
1289
+ catch (err) {
1290
+ onError(err instanceof Error ? err.message : String(err));
1291
+ }
1466
1292
  }
1467
-
1468
1293
  /** 轨迹回放面板的数据状态:会话列表 + 选中 + 事件 + 轮询 + 加载/错误。
1469
1294
  * 隐藏时暂停轮询,可见时按 REPLAY_POLL_MS 拉取并自动选当前会话。 */
1470
1295
  function useReplayDataState(props) {
1471
- const currentSession = props?.scope?.sessionId || ''
1472
- const visible = props?.visible !== false
1473
- const [sessions, setSessions] = useState([])
1474
- const [selected, setSelected] = useState('')
1475
- const [events, setEvents] = useState([])
1476
- const [loading, setLoading] = useState(true)
1477
- const [error, setError] = useState('')
1478
- const [reloadTick, setReloadTick] = useState(0)
1479
- const [resource, setResource] = useState(null)
1480
-
1481
- // 资源采样轮询(写放大/资源超限预警;可见时 15s 一次,隐藏暂停)
1482
- useEffect(() => {
1483
- if (!visible) return undefined
1484
- let alive = true
1485
- const tick = () => {
1486
- if (alive)
1487
- apiJson('/observability/api/resources')
1488
- .then(setResource)
1489
- .catch(() => {})
1490
- }
1491
- tick()
1492
- const timer = setInterval(tick, RESOURCE_POLL_MS)
1493
- return () => {
1494
- alive = false
1495
- clearInterval(timer)
1496
- }
1497
- }, [visible])
1498
-
1499
- useEffect(() => {
1500
- if (!visible) return undefined
1501
- let alive = true
1502
- const setters = { setSessions, setSelected, setEvents, setError, setLoading }
1503
- const tick = () => {
1504
- if (alive) void loadReplayData(selected, currentSession, setters)
1505
- }
1506
- tick()
1507
- const timer = setInterval(tick, REPLAY_POLL_MS)
1508
- return () => {
1509
- alive = false
1510
- clearInterval(timer)
1511
- }
1512
- }, [visible, selected, currentSession, reloadTick])
1513
-
1514
- const retry = () => {
1515
- setError('')
1516
- setLoading(true)
1517
- setReloadTick((tick) => tick + 1)
1518
- }
1519
-
1520
- return {
1521
- currentSession,
1522
- resource,
1523
- sessions,
1524
- selected,
1525
- events,
1526
- loading,
1527
- error,
1528
- setSessions,
1529
- setSelected,
1530
- setEvents,
1531
- setLoading,
1532
- setError,
1533
- retry,
1534
- }
1296
+ const currentSession = props?.scope?.sessionId || '';
1297
+ const visible = props?.visible !== false;
1298
+ const [sessions, setSessions] = useState([]);
1299
+ const [selected, setSelected] = useState('');
1300
+ const [events, setEvents] = useState([]);
1301
+ const [loading, setLoading] = useState(true);
1302
+ const [error, setError] = useState('');
1303
+ const [reloadTick, setReloadTick] = useState(0);
1304
+ const [resource, setResource] = useState(null);
1305
+ // 资源采样轮询(写放大/资源超限预警;可见时 15s 一次,隐藏暂停)
1306
+ useEffect(() => {
1307
+ if (!visible)
1308
+ return undefined;
1309
+ let alive = true;
1310
+ const tick = () => {
1311
+ if (alive)
1312
+ apiJson('/observability/api/resources')
1313
+ .then(setResource)
1314
+ .catch(() => { });
1315
+ };
1316
+ tick();
1317
+ const timer = setInterval(tick, RESOURCE_POLL_MS);
1318
+ return () => {
1319
+ alive = false;
1320
+ clearInterval(timer);
1321
+ };
1322
+ }, [visible]);
1323
+ useEffect(() => {
1324
+ if (!visible)
1325
+ return undefined;
1326
+ let alive = true;
1327
+ const setters = { setSessions, setSelected, setEvents, setError, setLoading };
1328
+ const tick = () => {
1329
+ if (alive)
1330
+ void loadReplayData(selected, currentSession, setters);
1331
+ };
1332
+ tick();
1333
+ const timer = setInterval(tick, REPLAY_POLL_MS);
1334
+ return () => {
1335
+ alive = false;
1336
+ clearInterval(timer);
1337
+ };
1338
+ }, [visible, selected, currentSession, reloadTick]);
1339
+ const retry = () => {
1340
+ setError('');
1341
+ setLoading(true);
1342
+ setReloadTick((tick) => tick + 1);
1343
+ };
1344
+ return {
1345
+ currentSession,
1346
+ resource,
1347
+ sessions,
1348
+ selected,
1349
+ events,
1350
+ loading,
1351
+ error,
1352
+ setSessions,
1353
+ setSelected,
1354
+ setEvents,
1355
+ setLoading,
1356
+ setError,
1357
+ retry,
1358
+ };
1535
1359
  }
1536
-
1537
1360
  /** 轨迹回放面板的视图状态:过滤条件(类型/关键词/时间/成功失败)+ 导出范围 +
1538
1361
  * 统计开关,及派生值(过滤结果 / 高亮行 / 可导出)。 */
1539
1362
  function useReplayState(props) {
1540
- const data = useReplayDataState(props)
1541
- const [filter, setFilter] = useState('')
1542
- const [keyword, setKeyword] = useState('')
1543
- const [timeStart, setTimeStart] = useState('')
1544
- const [timeEnd, setTimeEnd] = useState('')
1545
- const [result, setResult] = useState('')
1546
- const [scope, setScope] = useState('session')
1547
- const [showStats, setShowStats] = useState(false)
1548
-
1549
- const clearFilters = () => {
1550
- setKeyword('')
1551
- setTimeStart('')
1552
- setTimeEnd('')
1553
- setResult('')
1554
- setFilter('')
1555
- }
1556
-
1557
- const criteria = {
1558
- type: filter,
1559
- keyword,
1560
- timeStart: datetimeToMs(timeStart),
1561
- timeEnd: datetimeToMs(timeEnd),
1562
- result,
1563
- }
1564
- const filtered = applyAuditFilter(data.events, criteria)
1565
- const hasFilter = hasActiveFilter(criteria)
1566
- const rows = filtered.map((event, index) => createElement(EventRow, { key: event.id ?? index, event, keyword }))
1567
- const canExport = !data.loading && data.events.length > 0
1568
- const onExport = (format) => void runExport(format, scope, filtered, criteria, data.setError)
1569
-
1570
- return {
1571
- resource: data.resource,
1572
- sessions: data.sessions,
1573
- selected: data.selected,
1574
- events: data.events,
1575
- loading: data.loading,
1576
- error: data.error,
1577
- filter,
1578
- keyword,
1579
- timeStart,
1580
- timeEnd,
1581
- result,
1582
- scope,
1583
- showStats,
1584
- setSelected: data.setSelected,
1585
- setFilter,
1586
- setKeyword,
1587
- setTimeStart,
1588
- setTimeEnd,
1589
- setResult,
1590
- setScope,
1591
- setShowStats,
1592
- retry: data.retry,
1593
- clearFilters,
1594
- filtered,
1595
- hasFilter,
1596
- rows,
1597
- canExport,
1598
- onExport,
1599
- }
1600
- }
1601
-
1602
- // ── Git 工具 + 增量 diff 审查面板 ──────────────────────────────────
1603
- const REPO_KEY = 'dsh-my-observability:repo'
1604
- const COMMIT_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
1605
-
1363
+ const data = useReplayDataState(props);
1364
+ const [filter, setFilter] = useState('');
1365
+ const [keyword, setKeyword] = useState('');
1366
+ const [timeStart, setTimeStart] = useState('');
1367
+ const [timeEnd, setTimeEnd] = useState('');
1368
+ const [result, setResult] = useState('');
1369
+ const [scope, setScope] = useState('session');
1370
+ const [showStats, setShowStats] = useState(false);
1371
+ const clearFilters = () => {
1372
+ setKeyword('');
1373
+ setTimeStart('');
1374
+ setTimeEnd('');
1375
+ setResult('');
1376
+ setFilter('');
1377
+ };
1378
+ const criteria = {
1379
+ type: filter,
1380
+ keyword,
1381
+ timeStart: datetimeToMs(timeStart),
1382
+ timeEnd: datetimeToMs(timeEnd),
1383
+ result,
1384
+ };
1385
+ const filtered = applyAuditFilter(data.events, criteria);
1386
+ const hasFilter = hasActiveFilter(criteria);
1387
+ const rows = filtered.map((event, index) => createElement(EventRow, { key: event.id ?? index, event, keyword }));
1388
+ const canExport = !data.loading && data.events.length > 0;
1389
+ const onExport = (format) => void runExport(format, scope, filtered, criteria, data.setError);
1390
+ return {
1391
+ resource: data.resource,
1392
+ sessions: data.sessions,
1393
+ selected: data.selected,
1394
+ events: data.events,
1395
+ loading: data.loading,
1396
+ error: data.error,
1397
+ filter,
1398
+ keyword,
1399
+ timeStart,
1400
+ timeEnd,
1401
+ result,
1402
+ scope,
1403
+ showStats,
1404
+ setSelected: data.setSelected,
1405
+ setFilter,
1406
+ setKeyword,
1407
+ setTimeStart,
1408
+ setTimeEnd,
1409
+ setResult,
1410
+ setScope,
1411
+ setShowStats,
1412
+ retry: data.retry,
1413
+ clearFilters,
1414
+ filtered,
1415
+ hasFilter,
1416
+ rows,
1417
+ canExport,
1418
+ onExport,
1419
+ };
1420
+ }
1421
+
1422
+ "use strict";
1423
+ // ── Git 工具 + 增量 diff 审查面板 ──────────────────────────────────
1424
+ const REPO_KEY = 'dsh-my-observability:repo';
1425
+ const COMMIT_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'];
1606
1426
  function loadRepoKey() {
1607
- try {
1608
- const value = window.localStorage.getItem(REPO_KEY)
1609
- return typeof value === 'string' ? value : ''
1610
- } catch {
1611
- return ''
1612
- }
1427
+ try {
1428
+ const value = window.localStorage.getItem(REPO_KEY);
1429
+ return typeof value === 'string' ? value : '';
1430
+ }
1431
+ catch {
1432
+ return '';
1433
+ }
1613
1434
  }
1614
-
1615
1435
  function saveRepoKey(repo) {
1616
- try {
1617
- window.localStorage.setItem(REPO_KEY, repo)
1618
- } catch {
1619
- // storage is best-effort
1620
- }
1436
+ try {
1437
+ window.localStorage.setItem(REPO_KEY, repo);
1438
+ }
1439
+ catch {
1440
+ // storage is best-effort
1441
+ }
1621
1442
  }
1622
-
1623
1443
  /** 状态条:分支 + 变更计数。 */
1624
1444
  function StatusBar({ status }) {
1625
- if (status === null) return null
1626
- const parts = [`${strings.branch()} ${status.branch}`]
1627
- if (status.clean) parts.push(strings.clean())
1628
- else {
1629
- if (status.stagedCount > 0) parts.push(`${status.stagedCount} ${strings.staged()}`)
1630
- if (status.unstagedCount > 0) parts.push(`${status.unstagedCount} ${strings.unstaged()}`)
1631
- }
1632
- return createElement('div', { className: 'dsh-my-observability-status' }, parts.join(' · '))
1445
+ if (status === null)
1446
+ return null;
1447
+ const parts = [`${strings.branch()} ${status.branch}`];
1448
+ if (status.clean)
1449
+ parts.push(strings.clean());
1450
+ else {
1451
+ if (status.stagedCount > 0)
1452
+ parts.push(`${status.stagedCount} ${strings.staged()}`);
1453
+ if (status.unstagedCount > 0)
1454
+ parts.push(`${status.unstagedCount} ${strings.unstaged()}`);
1455
+ }
1456
+ return createElement('div', { className: 'dsh-my-observability-status' }, parts.join(' · '));
1633
1457
  }
1634
-
1635
1458
  /** 差异文本预览。 */
1636
1459
  function DiffView({ diff }) {
1637
- return createElement(
1638
- 'div',
1639
- { className: 'dsh-my-observability-section' },
1640
- createElement('div', { className: 'dsh-my-observability-section-title' }, strings.diffTitle()),
1641
- createElement('pre', { className: 'dsh-my-observability-diff' }, diff !== '' ? diff : strings.emptyDiff()),
1642
- )
1460
+ return createElement('div', { className: 'dsh-my-observability-section' }, createElement('div', { className: 'dsh-my-observability-section-title' }, strings.diffTitle()), createElement('pre', { className: 'dsh-my-observability-diff' }, diff !== '' ? diff : strings.emptyDiff()));
1643
1461
  }
1644
-
1645
1462
  /** 严重级别 → 中文。 */
1646
1463
  function severityText(severity) {
1647
- if (severity === 'error') return strings.severityError()
1648
- if (severity === 'warning') return strings.severityWarning()
1649
- return strings.severityInfo()
1464
+ if (severity === 'error')
1465
+ return strings.severityError();
1466
+ if (severity === 'warning')
1467
+ return strings.severityWarning();
1468
+ return strings.severityInfo();
1650
1469
  }
1651
-
1652
1470
  /** AI 结论文本(未启用/失败/成功三态,尽力而为)。 */
1653
1471
  function aiTextOf(ai) {
1654
- if (ai === undefined || ai === null || !ai.enabled) return ''
1655
- if (ai.failed) return `${strings.aiFailed()}(${ai.note ?? ''})`
1656
- return ai.verdict === 'approve' ? strings.aiVerdictApprove() : strings.aiVerdictChanges()
1472
+ if (ai === undefined || ai === null || !ai.enabled)
1473
+ return '';
1474
+ if (ai.failed)
1475
+ return `${strings.aiFailed()}(${ai.note ?? ''})`;
1476
+ return ai.verdict === 'approve' ? strings.aiVerdictApprove() : strings.aiVerdictChanges();
1657
1477
  }
1658
-
1659
1478
  /** 审查报告:问题列表 + AI 结论。 */
1660
1479
  function ReviewReport({ report }) {
1661
- if (report === null) return null
1662
- const issues = report.issues || []
1663
- const rows = issues.map((issue, index) =>
1664
- createElement(
1665
- 'div',
1666
- {
1480
+ if (report === null)
1481
+ return null;
1482
+ const issues = report.issues || [];
1483
+ const rows = issues.map((issue, index) => createElement('div', {
1667
1484
  key: index,
1668
1485
  className: `dsh-my-observability-issue dsh-my-observability-issue-${issue.severity}`,
1669
- },
1670
- createElement('span', { className: 'dsh-my-observability-issue-sev' }, severityText(issue.severity)),
1671
- createElement(
1672
- 'span',
1673
- { className: 'dsh-my-observability-issue-rule' },
1674
- `${issue.rule}${issue.file !== '' ? ` ${issue.file}:${issue.line}` : ''}`,
1675
- ),
1676
- createElement('span', { className: 'dsh-my-observability-issue-msg' }, issue.message),
1677
- ),
1678
- )
1679
- const aiText = aiTextOf(report.ai)
1680
- return createElement(
1681
- 'div',
1682
- { className: 'dsh-my-observability-section' },
1683
- createElement('div', { className: 'dsh-my-observability-section-title' }, strings.reviewResult()),
1684
- issues.length === 0
1685
- ? createElement('div', { className: 'dsh-my-observability-review-ok' }, strings.reviewPass())
1686
- : null,
1687
- rows,
1688
- aiText !== '' ? createElement('div', { className: 'dsh-my-observability-ai' }, aiText) : null,
1689
- )
1486
+ }, createElement('span', { className: 'dsh-my-observability-issue-sev' }, severityText(issue.severity)), createElement('span', { className: 'dsh-my-observability-issue-rule' }, `${issue.rule}${issue.file !== '' ? ` ${issue.file}:${issue.line}` : ''}`), createElement('span', { className: 'dsh-my-observability-issue-msg' }, issue.message)));
1487
+ const aiText = aiTextOf(report.ai);
1488
+ return createElement('div', { className: 'dsh-my-observability-section' }, createElement('div', { className: 'dsh-my-observability-section-title' }, strings.reviewResult()), issues.length === 0
1489
+ ? createElement('div', { className: 'dsh-my-observability-review-ok' }, strings.reviewPass())
1490
+ : null, rows, aiText !== '' ? createElement('div', { className: 'dsh-my-observability-ai' }, aiText) : null);
1690
1491
  }
1691
-
1692
1492
  /** 提交表单字段(type/scope/description/body + 提交按钮)。 */
1693
- function CommitFields({ form, update, busy, submit }) {
1694
- return createElement(
1695
- 'div',
1696
- { className: 'dsh-my-observability-form' },
1697
- createElement(
1698
- 'select',
1699
- {
1493
+ function CommitFields({ form, update, busy, submit, }) {
1494
+ return createElement('div', { className: 'dsh-my-observability-form' }, createElement('select', {
1700
1495
  className: 'dsh-my-observability-select dsh-my-observability-type',
1701
1496
  value: form.type,
1702
1497
  onChange: update('type'),
1703
- },
1704
- COMMIT_TYPES.map((type) => createElement('option', { key: type, value: type }, type)),
1705
- ),
1706
- createElement('input', {
1707
- className: 'dsh-my-observability-input',
1708
- placeholder: strings.commitScope(),
1709
- value: form.scope,
1710
- onChange: update('scope'),
1711
- }),
1712
- createElement('input', {
1713
- className: 'dsh-my-observability-input',
1714
- placeholder: strings.commitDesc(),
1715
- value: form.description,
1716
- onChange: update('description'),
1717
- }),
1718
- createElement('textarea', {
1719
- className: 'dsh-my-observability-input dsh-my-observability-textarea',
1720
- placeholder: strings.commitBody(),
1721
- value: form.body,
1722
- onChange: update('body'),
1723
- }),
1724
- createElement(
1725
- 'div',
1726
- { className: 'dsh-my-observability-actions' },
1727
- createElement(
1728
- 'button',
1729
- {
1730
- className: 'dsh-my-observability-btn dsh-my-observability-btn-primary',
1731
- disabled: busy,
1732
- onClick: submit,
1733
- },
1734
- strings.commit(),
1735
- ),
1736
- ),
1737
- )
1498
+ }, COMMIT_TYPES.map((type) => createElement('option', { key: type, value: type }, type))), createElement('input', {
1499
+ className: 'dsh-my-observability-input',
1500
+ placeholder: strings.commitScope(),
1501
+ value: form.scope,
1502
+ onChange: update('scope'),
1503
+ }), createElement('input', {
1504
+ className: 'dsh-my-observability-input',
1505
+ placeholder: strings.commitDesc(),
1506
+ value: form.description,
1507
+ onChange: update('description'),
1508
+ }), createElement('textarea', {
1509
+ className: 'dsh-my-observability-input dsh-my-observability-textarea',
1510
+ placeholder: strings.commitBody(),
1511
+ value: form.body,
1512
+ onChange: update('body'),
1513
+ }), createElement('div', { className: 'dsh-my-observability-actions' }, createElement('button', {
1514
+ className: 'dsh-my-observability-btn dsh-my-observability-btn-primary',
1515
+ disabled: busy,
1516
+ onClick: submit,
1517
+ }, strings.commit())));
1738
1518
  }
1739
-
1740
1519
  /** 类型化提交表单:type/scope/description/body → POST /git/commit。 */
1741
1520
  function CommitForm({ repo, onCommitted }) {
1742
- const [form, setForm] = useState({ type: 'feat', scope: '', description: '', body: '' })
1743
- const [busy, setBusy] = useState(false)
1744
- const [feedback, setFeedback] = useState('')
1745
- const [feedbackKind, setFeedbackKind] = useState('ok')
1746
- const update = (key) => (e) => setForm({ ...form, [key]: e.target.value })
1747
- const submit = async () => {
1748
- if (form.description.trim() === '') {
1749
- setFeedback(strings.commitError())
1750
- setFeedbackKind('error')
1751
- return
1752
- }
1753
- setBusy(true)
1754
- try {
1755
- const value = await apiJson('/observability/api/git/commit', {
1756
- method: 'POST',
1757
- headers: { 'content-type': 'application/json' },
1758
- body: JSON.stringify({ repoPath: repo, ...form }),
1759
- })
1760
- setFeedback(`${strings.committed()}:${value.hash} ${value.message}`)
1761
- setFeedbackKind('ok')
1762
- setForm({ ...form, scope: '', description: '', body: '' })
1763
- onCommitted()
1764
- } catch (err) {
1765
- setFeedback(`${strings.commitError()}:${err instanceof Error ? err.message : String(err)}`)
1766
- setFeedbackKind('error')
1767
- } finally {
1768
- setBusy(false)
1769
- }
1770
- }
1771
- return createElement(
1772
- 'div',
1773
- { className: 'dsh-my-observability-section' },
1774
- createElement('div', { className: 'dsh-my-observability-section-title' }, strings.commitTitle()),
1775
- createElement(CommitFields, { form, update, busy, submit }),
1776
- feedback !== ''
1777
- ? createElement(
1778
- 'div',
1779
- { className: `dsh-my-observability-feedback dsh-my-observability-feedback-${feedbackKind}` },
1780
- feedback,
1781
- )
1782
- : null,
1783
- )
1521
+ const [form, setForm] = useState({ type: 'feat', scope: '', description: '', body: '' });
1522
+ const [busy, setBusy] = useState(false);
1523
+ const [feedback, setFeedback] = useState('');
1524
+ const [feedbackKind, setFeedbackKind] = useState('ok');
1525
+ const update = (key) => (e) => setForm({ ...form, [key]: e.target.value });
1526
+ const submit = async () => {
1527
+ if (form.description.trim() === '') {
1528
+ setFeedback(strings.commitError());
1529
+ setFeedbackKind('error');
1530
+ return;
1531
+ }
1532
+ setBusy(true);
1533
+ try {
1534
+ const value = await apiJson('/observability/api/git/commit', {
1535
+ method: 'POST',
1536
+ headers: { 'content-type': 'application/json' },
1537
+ body: JSON.stringify({ repoPath: repo, ...form }),
1538
+ });
1539
+ setFeedback(`${strings.committed()}:${value.hash} ${value.message}`);
1540
+ setFeedbackKind('ok');
1541
+ setForm({ ...form, scope: '', description: '', body: '' });
1542
+ onCommitted();
1543
+ }
1544
+ catch (err) {
1545
+ setFeedback(`${strings.commitError()}:${err instanceof Error ? err.message : String(err)}`);
1546
+ setFeedbackKind('error');
1547
+ }
1548
+ finally {
1549
+ setBusy(false);
1550
+ }
1551
+ };
1552
+ return createElement('div', { className: 'dsh-my-observability-section' }, createElement('div', { className: 'dsh-my-observability-section-title' }, strings.commitTitle()), createElement(CommitFields, { form, update, busy, submit }), feedback !== ''
1553
+ ? createElement('div', { className: `dsh-my-observability-feedback dsh-my-observability-feedback-${feedbackKind}` }, feedback)
1554
+ : null);
1784
1555
  }
1785
-
1786
1556
  /** 仓库路径行:输入 + 加载按钮。 */
1787
- function RepoRow({ repo, onRepoChange, onLoad }) {
1788
- return createElement(
1789
- 'div',
1790
- { className: 'dsh-my-observability-repo-row' },
1791
- createElement('input', {
1792
- className: 'dsh-my-observability-input dsh-my-observability-repo-input',
1793
- placeholder: strings.repoPlaceholder(),
1794
- value: repo,
1795
- onChange: (e) => onRepoChange(e.target.value),
1796
- }),
1797
- createElement('button', { className: 'dsh-my-observability-btn', onClick: onLoad }, strings.loadRepo()),
1798
- )
1557
+ function RepoRow({ repo, onRepoChange, onLoad, }) {
1558
+ return createElement('div', { className: 'dsh-my-observability-repo-row' }, createElement('input', {
1559
+ className: 'dsh-my-observability-input dsh-my-observability-repo-input',
1560
+ placeholder: strings.repoPlaceholder(),
1561
+ value: repo,
1562
+ onChange: (e) => onRepoChange(e.target.value),
1563
+ }), createElement('button', { className: 'dsh-my-observability-btn', onClick: onLoad }, strings.loadRepo()));
1799
1564
  }
1800
-
1801
1565
  /** 操作按钮组:diff / staged diff / 审查。 */
1802
1566
  function GitActions({ onDiff, onReview }) {
1803
- return createElement(
1804
- 'div',
1805
- { className: 'dsh-my-observability-actions' },
1806
- createElement(
1807
- 'button',
1808
- { className: 'dsh-my-observability-btn', onClick: () => onDiff(false) },
1809
- strings.showDiff(),
1810
- ),
1811
- createElement(
1812
- 'button',
1813
- { className: 'dsh-my-observability-btn', onClick: () => onDiff(true) },
1814
- strings.showStagedDiff(),
1815
- ),
1816
- createElement(
1817
- 'button',
1818
- { className: 'dsh-my-observability-btn dsh-my-observability-btn-primary', onClick: onReview },
1819
- strings.review(),
1820
- ),
1821
- )
1567
+ return createElement('div', { className: 'dsh-my-observability-actions' }, createElement('button', { className: 'dsh-my-observability-btn', onClick: () => onDiff(false) }, strings.showDiff()), createElement('button', { className: 'dsh-my-observability-btn', onClick: () => onDiff(true) }, strings.showStagedDiff()), createElement('button', { className: 'dsh-my-observability-btn dsh-my-observability-btn-primary', onClick: onReview }, strings.review()));
1822
1568
  }
1823
-
1824
1569
  /** 拉取仓库状态(错误写入 setError)。 */
1825
1570
  async function fetchStatus(path, setStatus, setError) {
1826
- if (path === '') return
1827
- try {
1828
- setStatus(await apiJson(`/observability/api/git/status?repo=${encodeURIComponent(path)}`))
1829
- setError('')
1830
- } catch (err) {
1831
- setError(err instanceof Error ? err.message : String(err))
1832
- }
1571
+ if (path === '')
1572
+ return;
1573
+ try {
1574
+ setStatus(await apiJson(`/observability/api/git/status?repo=${encodeURIComponent(path)}`));
1575
+ setError('');
1576
+ }
1577
+ catch (err) {
1578
+ setError(err instanceof Error ? err.message : String(err));
1579
+ }
1833
1580
  }
1834
-
1835
1581
  /** 拉取差异文本(staged 切换;错误写入 setError)。 */
1836
1582
  async function fetchDiff(path, staged, setDiff, setError) {
1837
- if (path === '') return
1838
- try {
1839
- const value = await apiJson(`/observability/api/git/diff?repo=${encodeURIComponent(path)}&staged=${staged ? 1 : 0}`)
1840
- setDiff(value.text)
1841
- setError('')
1842
- } catch (err) {
1843
- setError(err instanceof Error ? err.message : String(err))
1844
- }
1583
+ if (path === '')
1584
+ return;
1585
+ try {
1586
+ const value = await apiJson(`/observability/api/git/diff?repo=${encodeURIComponent(path)}&staged=${staged ? 1 : 0}`);
1587
+ setDiff(value.text);
1588
+ setError('');
1589
+ }
1590
+ catch (err) {
1591
+ setError(err instanceof Error ? err.message : String(err));
1592
+ }
1845
1593
  }
1846
-
1847
1594
  /** 运行提交前审查(错误写入 setError)。 */
1848
1595
  async function runReview(path, setReport, setError) {
1849
- if (path === '') return
1850
- try {
1851
- setReport(
1852
- await apiJson('/observability/api/review', {
1853
- method: 'POST',
1854
- headers: { 'content-type': 'application/json' },
1855
- body: JSON.stringify({ repoPath: path }),
1856
- }),
1857
- )
1858
- setError('')
1859
- } catch (err) {
1860
- setError(err instanceof Error ? err.message : String(err))
1861
- }
1596
+ if (path === '')
1597
+ return;
1598
+ try {
1599
+ setReport(await apiJson('/observability/api/review', {
1600
+ method: 'POST',
1601
+ headers: { 'content-type': 'application/json' },
1602
+ body: JSON.stringify({ repoPath: path }),
1603
+ }));
1604
+ setError('');
1605
+ }
1606
+ catch (err) {
1607
+ setError(err instanceof Error ? err.message : String(err));
1608
+ }
1862
1609
  }
1863
-
1864
1610
  /** Git 面板主组件:仓库状态 / diff / 审查 / 类型化提交。 */
1865
1611
  function GitPanel() {
1866
- const [repo, setRepo] = useState(loadRepoKey)
1867
- const [status, setStatus] = useState(null)
1868
- const [diff, setDiff] = useState('')
1869
- const [report, setReport] = useState(null)
1870
- const [error, setError] = useState('')
1871
-
1872
- const onCommitted = async () => {
1873
- setDiff('')
1874
- setReport(null)
1875
- await fetchStatus(repo, setStatus, setError)
1876
- }
1877
-
1878
- return createElement(
1879
- 'div',
1880
- { className: 'dsh-my-observability-panel' },
1881
- createElement(RepoRow, {
1882
- repo,
1883
- onRepoChange: (value) => {
1884
- setRepo(value)
1885
- saveRepoKey(value)
1886
- },
1887
- onLoad: () => fetchStatus(repo, setStatus, setError),
1888
- }),
1889
- error !== '' ? createElement('div', { className: 'dsh-my-observability-empty' }, error) : null,
1890
- createElement(StatusBar, { status }),
1891
- createElement(GitActions, {
1892
- onDiff: (staged) => fetchDiff(repo, staged, setDiff, setError),
1893
- onReview: () => runReview(repo, setReport, setError),
1894
- }),
1895
- createElement(DiffView, { diff }),
1896
- createElement(ReviewReport, { report }),
1897
- createElement(CommitForm, { repo, onCommitted }),
1898
- )
1612
+ const [repo, setRepo] = useState(loadRepoKey);
1613
+ const [status, setStatus] = useState(null);
1614
+ const [diff, setDiff] = useState('');
1615
+ const [report, setReport] = useState(null);
1616
+ const [error, setError] = useState('');
1617
+ const onCommitted = async () => {
1618
+ setDiff('');
1619
+ setReport(null);
1620
+ await fetchStatus(repo, setStatus, setError);
1621
+ };
1622
+ return createElement('div', { className: 'dsh-my-observability-panel' }, createElement(RepoRow, {
1623
+ repo,
1624
+ onRepoChange: (value) => {
1625
+ setRepo(value);
1626
+ saveRepoKey(value);
1627
+ },
1628
+ onLoad: () => fetchStatus(repo, setStatus, setError),
1629
+ }), error !== '' ? createElement('div', { className: 'dsh-my-observability-empty' }, error) : null, createElement(StatusBar, { status }), createElement(GitActions, {
1630
+ onDiff: (staged) => fetchDiff(repo, staged, setDiff, setError),
1631
+ onReview: () => runReview(repo, setReport, setError),
1632
+ }), createElement(DiffView, { diff }), createElement(ReviewReport, { report }), createElement(CommitForm, { repo, onCommitted }));
1899
1633
  }
1900
1634
 
1901
- // ── 样式(DSH 语义 token,随 activation 注入 / teardown 卸载)──────
1635
+ "use strict";
1636
+ // ── 样式(DSH 语义 token,随 activation 注入 / teardown 卸载)──────
1902
1637
  // 前缀 dsh-my-observability-(issue #54:与 dsh-my-guard 前缀分离,消除跨插件类名冲突)。
1903
1638
  const STYLES = `
1904
1639
  .dsh-my-observability-panel{display:flex;flex-direction:column;gap:10px;padding:2px 6px 8px;color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-14)}
@@ -2042,49 +1777,60 @@ const STYLES = `
2042
1777
  .dsh-my-observability-resource-alert{font:var(--dsw-font-xxxs-11);border-radius:4px;padding:2px 6px}
2043
1778
  .dsh-my-observability-resource-alert-error{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}
2044
1779
  .dsh-my-observability-resource-alert-warn{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent)}
2045
- `
2046
-
1780
+ `;
2047
1781
  function injectStyles() {
2048
- if (typeof document === 'undefined' || typeof document.head === 'undefined') return () => {}
2049
- const style = document.createElement('style')
2050
- style.setAttribute('data-dsh-my-observability', 'styles')
2051
- style.textContent = STYLES
2052
- document.head.appendChild(style)
2053
- return () => {
2054
- if (style.parentNode !== null) style.parentNode.removeChild(style)
2055
- }
1782
+ if (typeof document === 'undefined' || typeof document.head === 'undefined')
1783
+ return () => { };
1784
+ const style = document.createElement('style');
1785
+ style.setAttribute('data-dsh-my-observability', 'styles');
1786
+ style.textContent = STYLES;
1787
+ document.head.appendChild(style);
1788
+ return () => {
1789
+ if (style.parentNode !== null)
1790
+ style.parentNode.removeChild(style);
1791
+ };
2056
1792
  }
2057
1793
 
2058
1794
 
2059
- // ── 插件体:样式注入 + 两个页签注册 ────────────────────────────────
2060
- exports.inject = ['betterSidebar']
1795
+ // ── 插件体:样式注入 + 两个原生页签注册 ─────────────────────────────
1796
+ exports.inject = ['slots', 'sidebarRightTabs']
1797
+
1798
+ /** 原生 tab body 席位:适配成面板契约(ReplayPanel 要 visible)。 */
1799
+ function ReplayTabBody(props) {
1800
+ const info = typeof props.useTabInfo === 'function' ? props.useTabInfo() : undefined
1801
+ return createElement(ReplayPanel, { visible: info?.tab?.visible !== false })
1802
+ }
1803
+
1804
+ /** Git 面板不接 props(迁移前组件工厂同样只是透传)。 */
1805
+ function GitTabBody() {
1806
+ return createElement(GitPanel, null)
1807
+ }
2061
1808
 
2062
1809
  exports.apply = function apply(ctx) {
1810
+ // 样式先注入:不依赖任何服务(服务缺失/时序未就绪也不影响面板配色)。
2063
1811
  ctx.effect(() => injectStyles(), 'dsh-my-observability: styles')
2064
- const service = ctx.betterSidebar
2065
- if (service === undefined) return
2066
- ctx.effect(
2067
- () =>
2068
- service.registerTab({
2069
- id: 'dsh-my-observability:replay',
2070
- title: () => strings.replayTitle(),
2071
- order: 40,
2072
- single: true,
2073
- component: (props) => createElement(ReplayPanel, props),
2074
- }),
2075
- 'dsh-my-observability: replay tab registration',
2076
- )
2077
- ctx.effect(
2078
- () =>
2079
- service.registerTab({
2080
- id: 'dsh-my-observability:git',
2081
- title: () => strings.gitTitle(),
2082
- order: 41,
2083
- single: true,
2084
- component: (props) => createElement(GitPanel, props),
2085
- }),
2086
- 'dsh-my-observability: git tab registration',
2087
- )
1812
+
1813
+ const tabs = ctx.sidebarRightTabs
1814
+ const slots = ctx.slots
1815
+ // 判空必须同时覆盖 null 与 undefined(typeof null 是 object,会骗过 === undefined)。
1816
+ if (tabs == null || slots == null) return
1817
+
1818
+ registerNativeTab(ctx, tabs, slots, {
1819
+ id: REPLAY_TAB_ID,
1820
+ order: REPLAY_TAB_ORDER,
1821
+ label: 'replay',
1822
+ title: () => strings.replayTitle(),
1823
+ Body: ReplayTabBody,
1824
+ Title: nativeTabTitle(() => strings.replayTitle()),
1825
+ })
1826
+ registerNativeTab(ctx, tabs, slots, {
1827
+ id: GIT_TAB_ID,
1828
+ order: GIT_TAB_ORDER,
1829
+ label: 'git',
1830
+ title: () => strings.gitTitle(),
1831
+ Body: GitTabBody,
1832
+ Title: nativeTabTitle(() => strings.gitTitle()),
1833
+ })
2088
1834
  }
2089
1835
 
2090
1836
  return module.exports