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/CHANGELOG.md +9 -79
- package/README.md +18 -76
- package/lib/ai.js +123 -99
- package/lib/audit-view.js +173 -172
- package/lib/audit.js +175 -166
- package/lib/client.js +1024 -1278
- package/lib/client.src.js +55 -29
- package/lib/constants.js +4 -4
- package/lib/diff.js +61 -56
- package/lib/git.js +97 -98
- package/lib/index.js +46 -47
- package/lib/parts/native-tabs.js +45 -0
- package/lib/resource-monitor.js +82 -134
- package/lib/resource-rules.js +9 -99
- package/lib/review.js +68 -77
- package/lib/routes.js +223 -200
- package/lib/store-persist.js +114 -111
- package/lib/store.js +215 -212
- package/package.json +15 -7
- package/lib/parts/git.js +0 -298
- package/lib/parts/i18n.js +0 -104
- package/lib/parts/replay-ext.js +0 -396
- package/lib/parts/replay.js +0 -386
- package/lib/parts/resource.js +0 -80
- package/lib/parts/styles.js +0 -156
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* client-parts,见 docs/UI规范.md
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
583
|
+
return String(n).padStart(2, '0');
|
|
499
584
|
}
|
|
500
|
-
|
|
501
585
|
/** 毫秒时间戳 → `YYYY-MM-DD HH:MM:SS`(本地时区);非法输入返回空串。 */
|
|
502
586
|
function formatTime(time) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
598
|
+
return [data.status, data.agentType].filter((part) => typeof part === 'string');
|
|
514
599
|
}
|
|
515
|
-
|
|
516
600
|
/** 模型流事件的搜索片段(含错误消息)。 */
|
|
517
601
|
function llmParts(data) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
595
|
-
typeof criteria.
|
|
596
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
639
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
759
|
+
return JSON.stringify(events ?? [], null, space);
|
|
672
760
|
}
|
|
673
|
-
|
|
674
761
|
/** 事件是否为工具类(tool_call / tool_result)。 */
|
|
675
762
|
function isToolEvent(event) {
|
|
676
|
-
|
|
763
|
+
return event?.type === 'tool_call' || event?.type === 'tool_result';
|
|
677
764
|
}
|
|
678
|
-
|
|
679
765
|
/** 把单条工具事件计入聚合(调用次数 / 失败次数)。 */
|
|
680
766
|
function bumpTool(byTool, event, name) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
710
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
916
|
+
return `状态 ${data.status} · ${agentTypeText(data.agentType)}`;
|
|
819
917
|
}
|
|
820
|
-
|
|
821
918
|
/** 模型流事件摘要(开始/结束/错误 + 统计)。 */
|
|
822
919
|
function llmMeta(data) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
837
|
-
|
|
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
|
-
|
|
843
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
988
|
-
|
|
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
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
'
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1210
|
-
const
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
'
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
1453
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
-
|
|
1671
|
-
|
|
1672
|
-
'
|
|
1673
|
-
{ className: 'dsh-my-observability-
|
|
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
|
-
|
|
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
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
createElement('
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
{ className: 'dsh-my-observability-section' },
|
|
1774
|
-
|
|
1775
|
-
|
|
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
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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 = ['
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
ctx.
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
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
|