dsh-my-observability 0.1.7 → 0.3.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 CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  本文件记录 dsh-my-observability 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.3.0] - 2026-09-07
6
+
7
+ ### 变更
8
+
9
+ - feat(observability): #155 错误上报统一——plugin_error 事件类型 + /errors API (#177)
10
+ - feat(observability): #155 资源监控扩展——新增 $DSH_HOME 目录总大小监控 (#176)
11
+ - feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
12
+ - chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
13
+
14
+ ## [0.2.0] - 2026-09-07
15
+
16
+ ### 变更
17
+
18
+ - feat(observability): #155 错误上报统一——plugin_error 事件类型 + /errors API (#177)
19
+ - feat(observability): #155 资源监控扩展——新增 $DSH_HOME 目录总大小监控 (#176)
20
+ - feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
21
+ - chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
22
+
5
23
  ## [0.1.7] - 2026-09-06
6
24
 
7
25
  ### 变更
package/lib/audit.js CHANGED
@@ -71,17 +71,21 @@ function summarizeParams(payload) {
71
71
  function handlePluginEvent(name, payload, record) {
72
72
  const sessionId = payload?.sessionId
73
73
  if (typeof sessionId !== 'string' || sessionId === '') return
74
- record({
75
- type: 'plugin_event',
76
- sessionId,
77
- data: {
78
- plugin: pluginNameOf(name),
79
- event: eventNameOf(name),
80
- action: truncate(String(payload?.action ?? '')),
81
- reason: truncate(String(payload?.reason ?? '')),
82
- params: summarizeParams(payload),
83
- },
84
- })
74
+ const { type, data } = buildPluginEventData(name, payload)
75
+ record({ type, sessionId, data })
76
+ }
77
+
78
+ function buildPluginEventData(name, payload) {
79
+ const data = {
80
+ plugin: pluginNameOf(name),
81
+ event: eventNameOf(name),
82
+ action: truncate(String(payload?.action ?? '')),
83
+ reason: truncate(String(payload?.reason ?? '')),
84
+ params: summarizeParams(payload),
85
+ }
86
+ const error = typeof payload?.error === 'string' && payload.error !== '' ? truncate(payload.error) : null
87
+ if (error !== null) data.error = error
88
+ return { type: error !== null ? 'plugin_error' : 'plugin_event', data }
85
89
  }
86
90
 
87
91
  /**
@@ -14,7 +14,9 @@
14
14
  * 采样自身开销:15s 一次 process.cpuUsage/memoryUsage + fs.stat(<0.01% CPU、
15
15
  * 零分配大对象),远低于「监控不能放大被监控对象」的护栏(resource-budget-review)。
16
16
  */
17
- import { statSync } from 'node:fs'
17
+ import { statSync, readdirSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import { homedir } from 'node:os'
18
20
  import { evaluateResourceAlerts, shouldEnterDegrade, shouldExitDegrade, DEFAULT_LIMITS } from './resource-rules.js'
19
21
  import { jsonlFile } from './store-persist.js'
20
22
 
@@ -31,6 +33,7 @@ export function createResourceMonitor(ctx, options = {}) {
31
33
  const state = {
32
34
  timer: null,
33
35
  file: jsonlFile(),
36
+ dshHome: process.env.DSH_HOME || join(homedir(), '.dsh'),
34
37
  lastSample: null,
35
38
  lastCpu: process.cpuUsage(),
36
39
  history: [],
@@ -59,6 +62,12 @@ function sample(state, limits, onDegrade, onRecover) {
59
62
  } catch {
60
63
  // 审计文件尚未创建:字节为 0
61
64
  }
65
+ let homeBytes = 0
66
+ try {
67
+ homeBytes = dshHomeSize(state.dshHome)
68
+ } catch {
69
+ // $DSH_HOME 不可达
70
+ }
62
71
  const prev = state.lastSample
63
72
  if (prev !== null) {
64
73
  const deltaMs = Math.max(now - prev.time, 1)
@@ -66,7 +75,7 @@ function sample(state, limits, onDegrade, onRecover) {
66
75
  const cpuPercent = (cpuDelta / 1000 / deltaMs) * 100
67
76
  const byteDelta = fileBytes - prev.fileBytes
68
77
  const writeRateBytesPerHour = byteDelta > 0 ? (byteDelta / deltaMs) * 3600 * 1000 : 0
69
- const sample = { time: now, cpuPercent, memoryBytes, fileBytes, writeRateBytesPerHour }
78
+ const sample = { time: now, cpuPercent, memoryBytes, fileBytes, writeRateBytesPerHour, homeBytes }
70
79
  state.history.push(sample)
71
80
  if (state.history.length > MAX_HISTORY) state.history.splice(0, state.history.length - MAX_HISTORY)
72
81
  state.lastSample = sample
@@ -78,7 +87,7 @@ function sample(state, limits, onDegrade, onRecover) {
78
87
  degraded: state.degraded,
79
88
  }
80
89
  }
81
- state.lastSample = { time: now, fileBytes, memoryBytes, cpuPercent: 0, writeRateBytesPerHour: 0 }
90
+ state.lastSample = { time: now, fileBytes, memoryBytes, cpuPercent: 0, writeRateBytesPerHour: 0, homeBytes }
82
91
  return { ...state.lastSample, history: [...state.history], alerts: [], degraded: state.degraded }
83
92
  }
84
93
 
@@ -116,3 +125,25 @@ function stopMonitor(state) {
116
125
  state.timer = null
117
126
  }
118
127
  }
128
+
129
+ /** 递归计算目录总字节(best-effort,跳过不可达文件)。 */
130
+ function dshHomeSize(dir) {
131
+ let total = 0
132
+ try {
133
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
134
+ const full = join(dir, entry.name)
135
+ if (entry.isDirectory()) {
136
+ total += dshHomeSize(full)
137
+ } else if (entry.isFile()) {
138
+ try {
139
+ total += statSync(full).size
140
+ } catch {
141
+ // 文件不可达
142
+ }
143
+ }
144
+ }
145
+ } catch {
146
+ // 目录不可达
147
+ }
148
+ return total
149
+ }
package/lib/routes.js CHANGED
@@ -67,6 +67,13 @@ function isMethod(method, request, name, verb) {
67
67
 
68
68
  /** 按 method 分派到具体 handler;未识别返回 false(调用方回 404)。 */
69
69
  async function dispatchMethod(method, request, response, url, ctx, store, monitor, options) {
70
+ const handled = await dispatchCore(method, request, response, url, ctx, store, monitor, options)
71
+ if (handled) return true
72
+ return dispatchExtended(method, request, response, url, ctx, store)
73
+ }
74
+
75
+ /** 核心路由(复杂度 ≤10)。 */
76
+ async function dispatchCore(method, request, response, url, ctx, store, monitor, options) {
70
77
  if (isMethod(method, request, 'sessions', 'GET')) {
71
78
  writeJson(response, 200, { ok: true, value: store.sessions() })
72
79
  return true
@@ -105,6 +112,19 @@ async function dispatchMethod(method, request, response, url, ctx, store, monito
105
112
  return false
106
113
  }
107
114
 
115
+ /** 扩展路由(#154/#155 新增)。 */
116
+ function dispatchExtended(method, request, response, url, ctx, store) {
117
+ if (isMethod(method, request, 'plugin-status', 'GET')) {
118
+ void handlePluginStatus(ctx, response)
119
+ return true
120
+ }
121
+ if (isMethod(method, request, 'errors', 'GET')) {
122
+ handleErrors(store, url, response)
123
+ return true
124
+ }
125
+ return false
126
+ }
127
+
108
128
  // ── handlers ───────────────────────────────────────────────────────────────
109
129
 
110
130
  /** 状态:审计统计 + 功能开关(aiReview 只暴露开关)。 */
@@ -117,6 +137,44 @@ function statusValue(store, options) {
117
137
  }
118
138
  }
119
139
 
140
+ /**
141
+ * 插件状态聚合:广播 plugin:status-query 事件,收集所有插件状态。
142
+ * 每个插件返回 { plugin, config, running, lastActions }(config 脱敏,lastActions ≤5)。
143
+ * 超时 3s 未响应的插件标记为 { plugin, running: false, error: 'timeout' }。
144
+ */
145
+ async function handlePluginStatus(ctx, response) {
146
+ const TIMEOUT_MS = 3000
147
+ const results = []
148
+ try {
149
+ // 广播查询事件,带超时
150
+ const statuses = await Promise.allSettled(
151
+ (ctx.bundler?.plugins ?? []).map(async (p) => {
152
+ const name = p.name ?? p.constructor?.name ?? 'unknown'
153
+ try {
154
+ const result = await Promise.race([
155
+ ctx.emit('plugin:status-query', { plugin: name }),
156
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS)),
157
+ ])
158
+ return result ?? { plugin: name, running: true, config: {}, lastActions: [] }
159
+ } catch {
160
+ return { plugin: name, running: false, error: 'timeout' }
161
+ }
162
+ }),
163
+ )
164
+ for (const s of statuses) {
165
+ results.push(s.status === 'fulfilled' ? s.value : { plugin: 'unknown', running: false, error: 'rejected' })
166
+ }
167
+ } catch {
168
+ // bundler 不可用时返回空列表
169
+ }
170
+ writeJson(response, 200, { ok: true, value: results })
171
+ }
172
+
173
+ /** 错误事件查询(#155 错误上报统一):返回 plugin_error 类型事件。 */
174
+ function handleErrors(store, url, response) {
175
+ writeJson(response, 200, { ok: true, value: store.events(null, 'plugin_error', limitOf(url)) })
176
+ }
177
+
120
178
  /** git status:非仓库路径 400。 */
121
179
  async function handleGitStatus(response, repoPath) {
122
180
  if (repoPath === '') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-my-observability",
3
- "version": "0.1.7",
3
+ "version": "0.3.0",
4
4
  "description": "DSH 可观测性 + Git 工程工具插件:会话轨迹回放(时间轴)、事件审计(agent 行为记录)、结构化 Git 提交(Conventional Commits)、增量 diff 审查(提交前审查)。DSH web plugin: trajectory replay timeline, event audit log, structured git commits, pre-commit incremental diff review.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -37,10 +37,7 @@
37
37
  "patch": "./cordis.patch.yml"
38
38
  },
39
39
  "client": {
40
- "platform": "web",
41
- "inject": [
42
- "@deepseek-ai/dsh-client-runtime"
43
- ]
40
+ "platform": "web"
44
41
  }
45
42
  },
46
43
  "peerDependencies": {