dsh-my-observability 0.1.0
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 +20 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/assets/git-panel.png +0 -0
- package/assets/replay-panel.png +0 -0
- package/cordis.patch.yml +10 -0
- package/lib/ai.js +165 -0
- package/lib/audit.js +150 -0
- package/lib/client.js +636 -0
- package/lib/client.src.js +62 -0
- package/lib/constants.js +7 -0
- package/lib/diff.js +86 -0
- package/lib/fence.js +60 -0
- package/lib/git.js +142 -0
- package/lib/index.js +48 -0
- package/lib/parts/git.js +240 -0
- package/lib/parts/i18n.js +67 -0
- package/lib/parts/replay.js +199 -0
- package/lib/parts/styles.js +68 -0
- package/lib/review.js +126 -0
- package/lib/routes.js +215 -0
- package/lib/store.js +231 -0
- package/package.json +59 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-my-observability — client half (browser). SOURCE TEMPLATE.
|
|
3
|
+
*
|
|
4
|
+
* 提供两个侧边栏页签:
|
|
5
|
+
* - 轨迹回放(dsh-my-observability:replay):按时间轴查看 agent 行为
|
|
6
|
+
* (agent 状态 / 模型流 / 工具调用与结果),支持会话切换与类型过滤,
|
|
7
|
+
* 数据来自 server 端事件审计(/observability/api/events);
|
|
8
|
+
* - Git 工具 + 增量 diff 审查(dsh-my-observability:git):仓库状态与
|
|
9
|
+
* 差异查看、类型化提交(Conventional Commits)、提交前规则引擎 +
|
|
10
|
+
* 可选 AI 审查(/observability/api/git/* 与 /observability/api/review)。
|
|
11
|
+
*
|
|
12
|
+
* 面板可见(visible)时轮询(REPLAY_POLL_MS),隐藏时暂停(省请求)。
|
|
13
|
+
* 样式走 DSH 语义 token(--dsw-alias-* / --dsw-font-*),随 activation
|
|
14
|
+
* 注入、fiber teardown 卸载(HMR/禁用无残留)。
|
|
15
|
+
*
|
|
16
|
+
* BUILD NOTE: 本文件是模板源码,不是 DSH 实际服务的文件。scripts/build.mjs
|
|
17
|
+
* 将四个片段文件(lib/parts/i18n.js / replay.js / git.js / styles.js,均为
|
|
18
|
+
* 无 import/export 的纯函数声明文本)经下方 __PART_*__ 占位符(函数式
|
|
19
|
+
* replaceAll,避免 $&/$1 特殊解释)拼接进 factory 作用域,写出
|
|
20
|
+
* lib/client.js —— 即 DSH 实际服务的产物。产物必须提交;CI 只对产物执行
|
|
21
|
+
* node --check(见 scripts/test-all.sh / .github/workflows/ci.yml)。
|
|
22
|
+
*/
|
|
23
|
+
window.__ModuleLoader__.load({
|
|
24
|
+
id: 'dsh-my-observability',
|
|
25
|
+
factory: (require) => {
|
|
26
|
+
var module = { exports: {} }
|
|
27
|
+
var exports = module.exports
|
|
28
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
29
|
+
const { createElement, useEffect, useState } = require('react')
|
|
30
|
+
|
|
31
|
+
// ── parts(scripts/build.mjs 拼接;顺序固定)───────────────────────
|
|
32
|
+
// ── i18n(浏览器语言判定)──────────────────────────────────────────
|
|
33
|
+
function isZh() {
|
|
34
|
+
try {
|
|
35
|
+
const lang = (navigator.language || 'en').toLowerCase()
|
|
36
|
+
return lang.startsWith('zh')
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const strings = {
|
|
43
|
+
replayTitle: () => (isZh() ? '轨迹回放' : 'Trajectory'),
|
|
44
|
+
gitTitle: () => (isZh() ? 'Git 工具' : 'Git Tools'),
|
|
45
|
+
allSessions: () => (isZh() ? '全部会话' : 'All sessions'),
|
|
46
|
+
filterAll: () => (isZh() ? '全部' : 'All'),
|
|
47
|
+
filterStatus: () => (isZh() ? '状态' : 'Status'),
|
|
48
|
+
filterLlm: () => (isZh() ? '模型流' : 'LLM'),
|
|
49
|
+
filterTools: () => (isZh() ? '工具' : 'Tools'),
|
|
50
|
+
emptyEvents: () => (isZh() ? '暂无审计事件——开始一段对话后,agent 的行为会出现在这里' : 'No audit events yet — agent activity will appear here after a conversation'),
|
|
51
|
+
loadError: () => (isZh() ? '加载失败' : 'Load failed'),
|
|
52
|
+
typeAgentStatus: () => (isZh() ? 'agent 状态' : 'agent status'),
|
|
53
|
+
typeLlmStream: () => (isZh() ? '模型流' : 'LLM stream'),
|
|
54
|
+
typeToolCall: () => (isZh() ? '工具调用' : 'tool call'),
|
|
55
|
+
typeToolResult: () => (isZh() ? '工具结果' : 'tool result'),
|
|
56
|
+
phaseStart: () => (isZh() ? '开始' : 'start'),
|
|
57
|
+
phaseEnd: () => (isZh() ? '结束' : 'end'),
|
|
58
|
+
phaseError: () => (isZh() ? '错误' : 'error'),
|
|
59
|
+
agentTop: () => (isZh() ? '顶层' : 'top'),
|
|
60
|
+
agentSub: () => (isZh() ? '子代理' : 'subagent'),
|
|
61
|
+
agentUnknown: () => (isZh() ? '未知' : 'unknown'),
|
|
62
|
+
toolOk: () => (isZh() ? '成功' : 'ok'),
|
|
63
|
+
toolFail: () => (isZh() ? '失败' : 'failed'),
|
|
64
|
+
// Git 面板
|
|
65
|
+
repoLabel: () => (isZh() ? '仓库路径' : 'Repo path'),
|
|
66
|
+
repoPlaceholder: () => (isZh() ? '如 /path/to/project' : 'e.g. /path/to/project'),
|
|
67
|
+
loadRepo: () => (isZh() ? '加载' : 'Load'),
|
|
68
|
+
branch: () => (isZh() ? '分支' : 'Branch'),
|
|
69
|
+
staged: () => (isZh() ? '已暂存' : 'staged'),
|
|
70
|
+
unstaged: () => (isZh() ? '未暂存' : 'unstaged'),
|
|
71
|
+
clean: () => (isZh() ? '工作区干净' : 'Working tree clean'),
|
|
72
|
+
diffTitle: () => (isZh() ? '差异' : 'Diff'),
|
|
73
|
+
showDiff: () => (isZh() ? '查看差异' : 'Show diff'),
|
|
74
|
+
showStagedDiff: () => (isZh() ? '查看暂存差异' : 'Staged diff'),
|
|
75
|
+
noChanges: () => (isZh() ? '没有变更' : 'No changes'),
|
|
76
|
+
review: () => (isZh() ? '提交前审查' : 'Review'),
|
|
77
|
+
reviewAi: () => (isZh() ? 'AI 审查' : 'AI review'),
|
|
78
|
+
reviewResult: () => (isZh() ? '审查结果' : 'Review result'),
|
|
79
|
+
reviewPass: () => (isZh() ? '未发现问题' : 'No issues found'),
|
|
80
|
+
issues: (count) => (isZh() ? `${count} 个问题` : `${count} issue(s)`),
|
|
81
|
+
commitTitle: () => (isZh() ? '类型化提交' : 'Typed commit'),
|
|
82
|
+
commitType: () => (isZh() ? '类型' : 'Type'),
|
|
83
|
+
commitScope: () => (isZh() ? '范围(可选)' : 'Scope (optional)'),
|
|
84
|
+
commitDesc: () => (isZh() ? '描述' : 'Description'),
|
|
85
|
+
commitBody: () => (isZh() ? '正文(可选)' : 'Body (optional)'),
|
|
86
|
+
commit: () => (isZh() ? '提交' : 'Commit'),
|
|
87
|
+
committed: () => (isZh() ? '已提交' : 'Committed'),
|
|
88
|
+
commitError: () => (isZh() ? '提交失败' : 'Commit failed'),
|
|
89
|
+
severityError: () => (isZh() ? '错误' : 'Error'),
|
|
90
|
+
severityWarning: () => (isZh() ? '警告' : 'Warning'),
|
|
91
|
+
severityInfo: () => (isZh() ? '提示' : 'Info'),
|
|
92
|
+
aiVerdictApprove: () => (isZh() ? 'AI 结论:可以提交' : 'AI verdict: approve'),
|
|
93
|
+
aiVerdictChanges: () => (isZh() ? 'AI 结论:建议修改' : 'AI verdict: changes'),
|
|
94
|
+
aiFailed: () => (isZh() ? 'AI 审查不可用' : 'AI review unavailable'),
|
|
95
|
+
loading: () => (isZh() ? '加载中…' : 'Loading…'),
|
|
96
|
+
emptyDiff: () => (isZh() ? '(空)' : '(empty)'),
|
|
97
|
+
noRepo: () => (isZh() ? '请输入仓库路径' : 'Enter a repo path'),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 轨迹回放面板(时间轴)──────────────────────────────────────────
|
|
101
|
+
const REPLAY_POLL_MS = 5000
|
|
102
|
+
|
|
103
|
+
/** 请求插件 API(非 2xx 抛错;返回响应 JSON 的 value 字段)。 */
|
|
104
|
+
function apiJson(path, options) {
|
|
105
|
+
return fetch(path, options).then(async (res) => {
|
|
106
|
+
const data = await res.json()
|
|
107
|
+
if (!res.ok) throw new Error(data.error?.message || `HTTP ${res.status}`)
|
|
108
|
+
return data.value
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 事件类型 → 中文标签。 */
|
|
113
|
+
function typeLabel(event) {
|
|
114
|
+
switch (event.type) {
|
|
115
|
+
case 'agent_status': return strings.typeAgentStatus()
|
|
116
|
+
case 'llm_stream': return strings.typeLlmStream()
|
|
117
|
+
case 'tool_call': return strings.typeToolCall()
|
|
118
|
+
case 'tool_result': return strings.typeToolResult()
|
|
119
|
+
default: return event.type
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 事件类型 → 徽标样式类别。 */
|
|
124
|
+
function badgeKind(event) {
|
|
125
|
+
if (event.type === 'agent_status') return 'status'
|
|
126
|
+
if (event.type === 'llm_stream') return 'llm'
|
|
127
|
+
return 'tool'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 时间戳 → HH:MM:SS。 */
|
|
131
|
+
function timeText(time) {
|
|
132
|
+
try {
|
|
133
|
+
const date = new Date(time)
|
|
134
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
135
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
136
|
+
} catch {
|
|
137
|
+
return ''
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** agent 类型标记 → 中文。 */
|
|
142
|
+
function agentTypeText(agentType) {
|
|
143
|
+
if (agentType === 'top') return strings.agentTop()
|
|
144
|
+
if (agentType === 'subagent') return strings.agentSub()
|
|
145
|
+
return strings.agentUnknown()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 模型流阶段 → 中文。 */
|
|
149
|
+
function phaseText(phase) {
|
|
150
|
+
if (phase === 'start') return strings.phaseStart()
|
|
151
|
+
if (phase === 'end') return strings.phaseEnd()
|
|
152
|
+
if (phase === 'error') return strings.phaseError()
|
|
153
|
+
return phase
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** agent 状态事件摘要。 */
|
|
157
|
+
function agentMeta(data) {
|
|
158
|
+
return `状态 ${data.status} · ${agentTypeText(data.agentType)}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 模型流事件摘要(开始/结束/错误 + 统计)。 */
|
|
162
|
+
function llmMeta(data) {
|
|
163
|
+
const stats = data.phase === 'start' ? '' : ` · ${data.chunks} chunks / ${data.chars} chars / ${data.ms}ms`
|
|
164
|
+
const error = data.message !== undefined ? `:${data.message}` : ''
|
|
165
|
+
return `${phaseText(data.phase)}${stats}${error}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 工具调用事件摘要(名称 + 参数摘要)。 */
|
|
169
|
+
function toolCallMeta(data) {
|
|
170
|
+
const args = data.args && data.args.summary !== undefined ? ` — ${data.args.summary}` : ''
|
|
171
|
+
return `${data.name}${args}`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 工具结果事件摘要(名称 + 成败 + 耗时)。 */
|
|
175
|
+
function toolResultMeta(data) {
|
|
176
|
+
const result = data.ok === false ? strings.toolFail() : strings.toolOk()
|
|
177
|
+
return `${data.name} · ${result} · ${data.ms}ms`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 事件 → 摘要文本(单行,尽力而为)。 */
|
|
181
|
+
function eventMeta(event) {
|
|
182
|
+
const data = event.data || {}
|
|
183
|
+
if (event.type === 'agent_status') return agentMeta(data)
|
|
184
|
+
if (event.type === 'llm_stream') return llmMeta(data)
|
|
185
|
+
if (event.type === 'tool_call') return toolCallMeta(data)
|
|
186
|
+
if (event.type === 'tool_result') return toolResultMeta(data)
|
|
187
|
+
return ''
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 单条事件行(徽标 + 时间 + 摘要)。 */
|
|
191
|
+
function EventRow({ event }) {
|
|
192
|
+
const meta = eventMeta(event)
|
|
193
|
+
return createElement('div', { className: 'dso-event' },
|
|
194
|
+
createElement('div', { className: 'dso-event-head' },
|
|
195
|
+
createElement('span', { className: `dso-badge dso-badge-${badgeKind(event)}` }, typeLabel(event)),
|
|
196
|
+
createElement('span', { className: 'dso-time' }, timeText(event.time)),
|
|
197
|
+
),
|
|
198
|
+
meta !== '' ? createElement('div', { className: 'dso-event-meta' }, meta) : null,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 类型过滤按钮组。 */
|
|
203
|
+
function TypeFilter({ filter, onFilter }) {
|
|
204
|
+
const options = [
|
|
205
|
+
['', strings.filterAll()],
|
|
206
|
+
['agent_status', strings.filterStatus()],
|
|
207
|
+
['llm_stream', strings.filterLlm()],
|
|
208
|
+
['tool', strings.filterTools()],
|
|
209
|
+
]
|
|
210
|
+
return createElement('div', { className: 'dso-filters' },
|
|
211
|
+
options.map(([value, label]) => createElement('button', {
|
|
212
|
+
key: value,
|
|
213
|
+
className: `dso-chip${filter === value ? ' dso-chip-active' : ''}`,
|
|
214
|
+
onClick: () => onFilter(value),
|
|
215
|
+
}, label)),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** 按过滤条件筛选事件(tool = tool_call + tool_result)。 */
|
|
220
|
+
function filterEvents(events, filter) {
|
|
221
|
+
if (filter === '') return events
|
|
222
|
+
return events.filter((event) => filter === 'tool'
|
|
223
|
+
? event.type === 'tool_call' || event.type === 'tool_result'
|
|
224
|
+
: event.type === filter)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 拉取会话列表与事件(选中为空时自动选当前/首个会话)。 */
|
|
228
|
+
async function loadReplayData(selected, currentSession, setters) {
|
|
229
|
+
try {
|
|
230
|
+
const list = await apiJson('/observability/api/sessions')
|
|
231
|
+
setters.setSessions(list)
|
|
232
|
+
if (selected === '' && list.length > 0) {
|
|
233
|
+
const preferred = list.some((s) => s.sessionId === currentSession) ? currentSession : list[0].sessionId
|
|
234
|
+
setters.setSelected(preferred)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
const query = selected !== ''
|
|
238
|
+
? `/observability/api/events?sessionId=${encodeURIComponent(selected)}&limit=300`
|
|
239
|
+
: '/observability/api/events?limit=0'
|
|
240
|
+
setters.setEvents(await apiJson(query))
|
|
241
|
+
setters.setError('')
|
|
242
|
+
} catch (err) {
|
|
243
|
+
setters.setError(err instanceof Error ? err.message : String(err))
|
|
244
|
+
} finally {
|
|
245
|
+
setters.setLoading(false)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 工具栏:会话选择 + 类型过滤。 */
|
|
250
|
+
function ReplayToolbar({ sessions, selected, onSelect, filter, onFilter }) {
|
|
251
|
+
return createElement('div', { className: 'dso-toolbar' },
|
|
252
|
+
createElement('select', {
|
|
253
|
+
className: 'dso-select',
|
|
254
|
+
value: selected,
|
|
255
|
+
onChange: (e) => onSelect(e.target.value),
|
|
256
|
+
},
|
|
257
|
+
sessions.length === 0
|
|
258
|
+
? createElement('option', { value: '' }, strings.allSessions())
|
|
259
|
+
: sessions.map((s) => createElement('option', { key: s.sessionId, value: s.sessionId }, s.sessionId)),
|
|
260
|
+
),
|
|
261
|
+
createElement(TypeFilter, { filter, onFilter }),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 轨迹回放主面板:会话选择 + 类型过滤 + 时间轴(可见时轮询)。 */
|
|
266
|
+
function ReplayPanel(props) {
|
|
267
|
+
const currentSession = props.scope?.sessionId || ''
|
|
268
|
+
const visible = props.visible !== false
|
|
269
|
+
const [sessions, setSessions] = useState([])
|
|
270
|
+
const [selected, setSelected] = useState('')
|
|
271
|
+
const [filter, setFilter] = useState('')
|
|
272
|
+
const [events, setEvents] = useState([])
|
|
273
|
+
const [loading, setLoading] = useState(true)
|
|
274
|
+
const [error, setError] = useState('')
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (!visible) return undefined
|
|
278
|
+
let alive = true
|
|
279
|
+
const setters = { setSessions, setSelected, setEvents, setError, setLoading }
|
|
280
|
+
const tick = () => { if (alive) void loadReplayData(selected, currentSession, setters) }
|
|
281
|
+
tick()
|
|
282
|
+
const timer = setInterval(tick, REPLAY_POLL_MS)
|
|
283
|
+
return () => { alive = false; clearInterval(timer) }
|
|
284
|
+
}, [visible, selected, currentSession])
|
|
285
|
+
|
|
286
|
+
const filtered = filterEvents(events, filter)
|
|
287
|
+
const rows = filtered.map((event, index) => createElement(EventRow, { key: event.id ?? index, event }))
|
|
288
|
+
|
|
289
|
+
return createElement('div', { className: 'dso-panel' },
|
|
290
|
+
createElement(ReplayToolbar, { sessions, selected, onSelect: setSelected, filter, onFilter: setFilter }),
|
|
291
|
+
error !== '' ? createElement('div', { className: 'dso-empty' }, `${strings.loadError()}:${error}`) : null,
|
|
292
|
+
loading && error === '' ? createElement('div', { className: 'dso-empty' }, strings.loading()) : null,
|
|
293
|
+
!loading && error === '' && filtered.length === 0
|
|
294
|
+
? createElement('div', { className: 'dso-empty' }, strings.emptyEvents())
|
|
295
|
+
: null,
|
|
296
|
+
createElement('div', { className: 'dso-timeline' }, rows),
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Git 工具 + 增量 diff 审查面板 ──────────────────────────────────
|
|
301
|
+
const REPO_KEY = 'dsh-my-observability:repo'
|
|
302
|
+
const COMMIT_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
|
|
303
|
+
|
|
304
|
+
function loadRepoKey() {
|
|
305
|
+
try {
|
|
306
|
+
const value = window.localStorage.getItem(REPO_KEY)
|
|
307
|
+
return typeof value === 'string' ? value : ''
|
|
308
|
+
} catch {
|
|
309
|
+
return ''
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function saveRepoKey(repo) {
|
|
314
|
+
try {
|
|
315
|
+
window.localStorage.setItem(REPO_KEY, repo)
|
|
316
|
+
} catch {
|
|
317
|
+
// storage is best-effort
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 状态条:分支 + 变更计数。 */
|
|
322
|
+
function StatusBar({ status }) {
|
|
323
|
+
if (status === null) return null
|
|
324
|
+
const parts = [`${strings.branch()} ${status.branch}`]
|
|
325
|
+
if (status.clean) parts.push(strings.clean())
|
|
326
|
+
else {
|
|
327
|
+
if (status.stagedCount > 0) parts.push(`${status.stagedCount} ${strings.staged()}`)
|
|
328
|
+
if (status.unstagedCount > 0) parts.push(`${status.unstagedCount} ${strings.unstaged()}`)
|
|
329
|
+
}
|
|
330
|
+
return createElement('div', { className: 'dso-status' }, parts.join(' · '))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** 差异文本预览。 */
|
|
334
|
+
function DiffView({ diff }) {
|
|
335
|
+
return createElement('div', { className: 'dso-section' },
|
|
336
|
+
createElement('div', { className: 'dso-section-title' }, strings.diffTitle()),
|
|
337
|
+
createElement('pre', { className: 'dso-diff' }, diff !== '' ? diff : strings.emptyDiff()),
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 严重级别 → 中文。 */
|
|
342
|
+
function severityText(severity) {
|
|
343
|
+
if (severity === 'error') return strings.severityError()
|
|
344
|
+
if (severity === 'warning') return strings.severityWarning()
|
|
345
|
+
return strings.severityInfo()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** AI 结论文本(未启用/失败/成功三态,尽力而为)。 */
|
|
349
|
+
function aiTextOf(ai) {
|
|
350
|
+
if (ai === undefined || ai === null || !ai.enabled) return ''
|
|
351
|
+
if (ai.failed) return `${strings.aiFailed()}(${ai.note ?? ''})`
|
|
352
|
+
return ai.verdict === 'approve' ? strings.aiVerdictApprove() : strings.aiVerdictChanges()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 审查报告:问题列表 + AI 结论。 */
|
|
356
|
+
function ReviewReport({ report }) {
|
|
357
|
+
if (report === null) return null
|
|
358
|
+
const issues = report.issues || []
|
|
359
|
+
const rows = issues.map((issue, index) => createElement('div', {
|
|
360
|
+
key: index,
|
|
361
|
+
className: `dso-issue dso-issue-${issue.severity}`,
|
|
362
|
+
},
|
|
363
|
+
createElement('span', { className: 'dso-issue-sev' }, severityText(issue.severity)),
|
|
364
|
+
createElement('span', { className: 'dso-issue-rule' },
|
|
365
|
+
`${issue.rule}${issue.file !== '' ? ` ${issue.file}:${issue.line}` : ''}`,
|
|
366
|
+
),
|
|
367
|
+
createElement('span', { className: 'dso-issue-msg' }, issue.message),
|
|
368
|
+
))
|
|
369
|
+
const aiText = aiTextOf(report.ai)
|
|
370
|
+
return createElement('div', { className: 'dso-section' },
|
|
371
|
+
createElement('div', { className: 'dso-section-title' }, strings.reviewResult()),
|
|
372
|
+
issues.length === 0 ? createElement('div', { className: 'dso-review-ok' }, strings.reviewPass()) : null,
|
|
373
|
+
rows,
|
|
374
|
+
aiText !== '' ? createElement('div', { className: 'dso-ai' }, aiText) : null,
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 提交表单字段(type/scope/description/body + 提交按钮)。 */
|
|
379
|
+
function CommitFields({ form, update, busy, submit }) {
|
|
380
|
+
return createElement('div', { className: 'dso-form' },
|
|
381
|
+
createElement('select', { className: 'dso-select dso-type', value: form.type, onChange: update('type') },
|
|
382
|
+
COMMIT_TYPES.map((type) => createElement('option', { key: type, value: type }, type)),
|
|
383
|
+
),
|
|
384
|
+
createElement('input', {
|
|
385
|
+
className: 'dso-input',
|
|
386
|
+
placeholder: strings.commitScope(),
|
|
387
|
+
value: form.scope,
|
|
388
|
+
onChange: update('scope'),
|
|
389
|
+
}),
|
|
390
|
+
createElement('input', {
|
|
391
|
+
className: 'dso-input',
|
|
392
|
+
placeholder: strings.commitDesc(),
|
|
393
|
+
value: form.description,
|
|
394
|
+
onChange: update('description'),
|
|
395
|
+
}),
|
|
396
|
+
createElement('textarea', {
|
|
397
|
+
className: 'dso-input dso-textarea',
|
|
398
|
+
placeholder: strings.commitBody(),
|
|
399
|
+
value: form.body,
|
|
400
|
+
onChange: update('body'),
|
|
401
|
+
}),
|
|
402
|
+
createElement('div', { className: 'dso-actions' },
|
|
403
|
+
createElement('button', {
|
|
404
|
+
className: 'dso-btn dso-btn-primary',
|
|
405
|
+
disabled: busy,
|
|
406
|
+
onClick: submit,
|
|
407
|
+
}, strings.commit()),
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** 类型化提交表单:type/scope/description/body → POST /git/commit。 */
|
|
413
|
+
function CommitForm({ repo, onCommitted }) {
|
|
414
|
+
const [form, setForm] = useState({ type: 'feat', scope: '', description: '', body: '' })
|
|
415
|
+
const [busy, setBusy] = useState(false)
|
|
416
|
+
const [feedback, setFeedback] = useState('')
|
|
417
|
+
const update = (key) => (e) => setForm({ ...form, [key]: e.target.value })
|
|
418
|
+
const submit = async () => {
|
|
419
|
+
if (form.description.trim() === '') {
|
|
420
|
+
setFeedback(strings.commitError())
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
setBusy(true)
|
|
424
|
+
try {
|
|
425
|
+
const value = await apiJson('/observability/api/git/commit', {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: { 'content-type': 'application/json' },
|
|
428
|
+
body: JSON.stringify({ repoPath: repo, ...form }),
|
|
429
|
+
})
|
|
430
|
+
setFeedback(`${strings.committed()}:${value.hash} ${value.message}`)
|
|
431
|
+
setForm({ ...form, scope: '', description: '', body: '' })
|
|
432
|
+
onCommitted()
|
|
433
|
+
} catch (err) {
|
|
434
|
+
setFeedback(`${strings.commitError()}:${err instanceof Error ? err.message : String(err)}`)
|
|
435
|
+
} finally {
|
|
436
|
+
setBusy(false)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return createElement('div', { className: 'dso-section' },
|
|
440
|
+
createElement('div', { className: 'dso-section-title' }, strings.commitTitle()),
|
|
441
|
+
createElement(CommitFields, { form, update, busy, submit }),
|
|
442
|
+
feedback !== '' ? createElement('div', { className: 'dso-feedback' }, feedback) : null,
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** 仓库路径行:输入 + 加载按钮。 */
|
|
447
|
+
function RepoRow({ repo, onRepoChange, onLoad }) {
|
|
448
|
+
return createElement('div', { className: 'dso-repo-row' },
|
|
449
|
+
createElement('input', {
|
|
450
|
+
className: 'dso-input dso-repo-input',
|
|
451
|
+
placeholder: strings.repoPlaceholder(),
|
|
452
|
+
value: repo,
|
|
453
|
+
onChange: (e) => onRepoChange(e.target.value),
|
|
454
|
+
}),
|
|
455
|
+
createElement('button', { className: 'dso-btn', onClick: onLoad }, strings.loadRepo()),
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** 操作按钮组:diff / staged diff / 审查。 */
|
|
460
|
+
function GitActions({ onDiff, onReview }) {
|
|
461
|
+
return createElement('div', { className: 'dso-actions' },
|
|
462
|
+
createElement('button', { className: 'dso-btn', onClick: () => onDiff(false) }, strings.showDiff()),
|
|
463
|
+
createElement('button', { className: 'dso-btn', onClick: () => onDiff(true) }, strings.showStagedDiff()),
|
|
464
|
+
createElement('button', { className: 'dso-btn dso-btn-primary', onClick: onReview }, strings.review()),
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** 拉取仓库状态(错误写入 setError)。 */
|
|
469
|
+
async function fetchStatus(path, setStatus, setError) {
|
|
470
|
+
if (path === '') return
|
|
471
|
+
try {
|
|
472
|
+
setStatus(await apiJson(`/observability/api/git/status?repo=${encodeURIComponent(path)}`))
|
|
473
|
+
setError('')
|
|
474
|
+
} catch (err) {
|
|
475
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** 拉取差异文本(staged 切换;错误写入 setError)。 */
|
|
480
|
+
async function fetchDiff(path, staged, setDiff, setError) {
|
|
481
|
+
if (path === '') return
|
|
482
|
+
try {
|
|
483
|
+
const value = await apiJson(`/observability/api/git/diff?repo=${encodeURIComponent(path)}&staged=${staged ? 1 : 0}`)
|
|
484
|
+
setDiff(value.text)
|
|
485
|
+
setError('')
|
|
486
|
+
} catch (err) {
|
|
487
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** 运行提交前审查(错误写入 setError)。 */
|
|
492
|
+
async function runReview(path, setReport, setError) {
|
|
493
|
+
if (path === '') return
|
|
494
|
+
try {
|
|
495
|
+
setReport(await apiJson('/observability/api/review', {
|
|
496
|
+
method: 'POST',
|
|
497
|
+
headers: { 'content-type': 'application/json' },
|
|
498
|
+
body: JSON.stringify({ repoPath: path }),
|
|
499
|
+
}))
|
|
500
|
+
setError('')
|
|
501
|
+
} catch (err) {
|
|
502
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Git 面板主组件:仓库状态 / diff / 审查 / 类型化提交。 */
|
|
507
|
+
function GitPanel() {
|
|
508
|
+
const [repo, setRepo] = useState(loadRepoKey)
|
|
509
|
+
const [status, setStatus] = useState(null)
|
|
510
|
+
const [diff, setDiff] = useState('')
|
|
511
|
+
const [report, setReport] = useState(null)
|
|
512
|
+
const [error, setError] = useState('')
|
|
513
|
+
|
|
514
|
+
const onCommitted = async () => {
|
|
515
|
+
setDiff('')
|
|
516
|
+
setReport(null)
|
|
517
|
+
await fetchStatus(repo, setStatus, setError)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return createElement('div', { className: 'dso-panel' },
|
|
521
|
+
createElement(RepoRow, {
|
|
522
|
+
repo,
|
|
523
|
+
onRepoChange: (value) => {
|
|
524
|
+
setRepo(value)
|
|
525
|
+
saveRepoKey(value)
|
|
526
|
+
},
|
|
527
|
+
onLoad: () => fetchStatus(repo, setStatus, setError),
|
|
528
|
+
}),
|
|
529
|
+
error !== '' ? createElement('div', { className: 'dso-empty' }, error) : null,
|
|
530
|
+
createElement(StatusBar, { status }),
|
|
531
|
+
createElement(GitActions, {
|
|
532
|
+
onDiff: (staged) => fetchDiff(repo, staged, setDiff, setError),
|
|
533
|
+
onReview: () => runReview(repo, setReport, setError),
|
|
534
|
+
}),
|
|
535
|
+
createElement(DiffView, { diff }),
|
|
536
|
+
createElement(ReviewReport, { report }),
|
|
537
|
+
createElement(CommitForm, { repo, onCommitted }),
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── 样式(DSH 语义 token,随 activation 注入 / teardown 卸载)──────
|
|
542
|
+
const STYLES = `
|
|
543
|
+
.dso-panel{display:flex;flex-direction:column;gap:10px;padding:12px;color:var(--dsw-alias-label-primary)}
|
|
544
|
+
.dso-toolbar{display:flex;flex-direction:column;gap:8px}
|
|
545
|
+
.dso-select{flex:1;min-width:0;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-primary);
|
|
546
|
+
background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 8px}
|
|
547
|
+
.dso-input{flex:1;min-width:0;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-primary);
|
|
548
|
+
background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 8px}
|
|
549
|
+
.dso-input::placeholder{color:var(--dsw-alias-label-tertiary)}
|
|
550
|
+
.dso-repo-row{display:flex;gap:8px;align-items:center}
|
|
551
|
+
.dso-repo-input{flex:1}
|
|
552
|
+
.dso-filters{display:flex;gap:6px;flex-wrap:wrap}
|
|
553
|
+
.dso-chip{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);background:transparent;
|
|
554
|
+
border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 10px;cursor:pointer}
|
|
555
|
+
.dso-chip-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-interactive-primary);
|
|
556
|
+
background:color-mix(in srgb, var(--dsw-alias-interactive-primary) 12%, transparent)}
|
|
557
|
+
.dso-timeline{display:flex;flex-direction:column;gap:6px;max-height:calc(100vh - 240px);overflow-y:auto}
|
|
558
|
+
.dso-event{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px}
|
|
559
|
+
.dso-event-head{display:flex;align-items:center;gap:8px;justify-content:space-between}
|
|
560
|
+
.dso-badge{flex:none;font:var(--dsw-font-xxxs-strong-11);border-radius:4px;padding:1px 6px}
|
|
561
|
+
.dso-badge-status{color:var(--dsw-alias-state-info-primary);background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent)}
|
|
562
|
+
.dso-badge-llm{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent)}
|
|
563
|
+
.dso-badge-tool{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)}
|
|
564
|
+
.dso-time{font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-tertiary)}
|
|
565
|
+
.dso-event-meta{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);line-height:1.6;word-break:break-word;margin-top:2px}
|
|
566
|
+
.dso-empty{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-tertiary);text-align:center;padding:16px 8px;line-height:1.7}
|
|
567
|
+
.dso-status{font:var(--dsw-font-xxs-strong-12);color:var(--dsw-alias-label-secondary)}
|
|
568
|
+
.dso-actions{display:flex;gap:8px;flex-wrap:wrap}
|
|
569
|
+
.dso-btn{font:var(--dsw-font-xxs-strong-12);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);
|
|
570
|
+
border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 12px;cursor:pointer}
|
|
571
|
+
.dso-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
572
|
+
.dso-btn:disabled{opacity:.5;cursor:default}
|
|
573
|
+
.dso-btn-primary{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-interactive-primary);
|
|
574
|
+
background:color-mix(in srgb, var(--dsw-alias-interactive-primary) 16%, transparent)}
|
|
575
|
+
.dso-section{display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:8px}
|
|
576
|
+
.dso-section-title{font:var(--dsw-font-xs-strong-13);color:var(--dsw-alias-label-primary)}
|
|
577
|
+
.dso-diff{max-height:240px;overflow:auto;font:var(--dsw-font-mono-xxs);font-size:11px;line-height:1.5;
|
|
578
|
+
color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);
|
|
579
|
+
border-radius:6px;padding:8px;white-space:pre-wrap;word-break:break-all}
|
|
580
|
+
.dso-form{display:flex;flex-direction:column;gap:6px}
|
|
581
|
+
.dso-type{flex:none;width:96px}
|
|
582
|
+
.dso-textarea{min-height:52px;resize:vertical;font:var(--dsw-font-xxs-12)}
|
|
583
|
+
.dso-feedback{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);word-break:break-all;line-height:1.5}
|
|
584
|
+
.dso-issue{display:flex;flex-direction:column;gap:2px;border-radius:6px;padding:6px 8px;font:var(--dsw-font-xxs-12)}
|
|
585
|
+
.dso-issue-error{background:color-mix(in srgb, var(--dsw-alias-state-danger-primary) 12%, transparent)}
|
|
586
|
+
.dso-issue-warning{background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent)}
|
|
587
|
+
.dso-issue-info{background:color-mix(in srgb, var(--dsw-alias-state-info-primary) 10%, transparent)}
|
|
588
|
+
.dso-issue-sev{font:var(--dsw-font-xxxs-strong-11);text-transform:uppercase}
|
|
589
|
+
.dso-issue-error .dso-issue-sev{color:var(--dsw-alias-state-danger-primary)}
|
|
590
|
+
.dso-issue-warning .dso-issue-sev{color:var(--dsw-alias-state-warn-primary)}
|
|
591
|
+
.dso-issue-info .dso-issue-sev{color:var(--dsw-alias-state-info-primary)}
|
|
592
|
+
.dso-issue-rule{font:var(--dsw-font-mono-xxs);font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
593
|
+
.dso-issue-msg{color:var(--dsw-alias-label-primary);line-height:1.5}
|
|
594
|
+
.dso-review-ok{font:var(--dsw-font-xxs-strong-12);color:var(--dsw-alias-state-success-primary)}
|
|
595
|
+
.dso-ai{font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);line-height:1.5;
|
|
596
|
+
border:1px dashed var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px}
|
|
597
|
+
`
|
|
598
|
+
|
|
599
|
+
function injectStyles() {
|
|
600
|
+
if (typeof document === 'undefined' || typeof document.head === 'undefined') return () => {}
|
|
601
|
+
const style = document.createElement('style')
|
|
602
|
+
style.setAttribute('data-dsh-my-observability', 'styles')
|
|
603
|
+
style.textContent = STYLES
|
|
604
|
+
document.head.appendChild(style)
|
|
605
|
+
return () => {
|
|
606
|
+
if (style.parentNode !== null) style.parentNode.removeChild(style)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
// ── 插件体:样式注入 + 两个页签注册 ────────────────────────────────
|
|
612
|
+
exports.inject = ['betterSidebar']
|
|
613
|
+
|
|
614
|
+
exports.apply = function apply(ctx) {
|
|
615
|
+
ctx.effect(() => injectStyles(), 'dsh-my-observability: styles')
|
|
616
|
+
const service = ctx.betterSidebar
|
|
617
|
+
if (service === undefined) return
|
|
618
|
+
ctx.effect(() => service.registerTab({
|
|
619
|
+
id: 'dsh-my-observability:replay',
|
|
620
|
+
title: () => strings.replayTitle(),
|
|
621
|
+
order: 40,
|
|
622
|
+
single: true,
|
|
623
|
+
component: (props) => createElement(ReplayPanel, props),
|
|
624
|
+
}), 'dsh-my-observability: replay tab registration')
|
|
625
|
+
ctx.effect(() => service.registerTab({
|
|
626
|
+
id: 'dsh-my-observability:git',
|
|
627
|
+
title: () => strings.gitTitle(),
|
|
628
|
+
order: 41,
|
|
629
|
+
single: true,
|
|
630
|
+
component: (props) => createElement(GitPanel, props),
|
|
631
|
+
}), 'dsh-my-observability: git tab registration')
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return module.exports
|
|
635
|
+
},
|
|
636
|
+
})
|