dsh-my-observability 0.1.7 → 0.3.1

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,36 @@
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
+ ## [Unreleased]
6
+
7
+ ### 变更
8
+
9
+ - refactor(observability): #198 资源看门狗改为消费 `dsh-shared` 的 `createResourceGuard`(第三批)——插件私有状态机/判定/ring buffer 删除,只保留宿主职责(采样源 + $DSH_HOME 字节维度 + 降级动作)。行为等价:降级在第 3 个连续超限窗口触发、恢复在第 3 个连续正常窗口触发、回调恰好一次、告警规则与顺序不变;新增 `monitor.stats()` / `monitor.history()` 与可注入采样源/时钟(确定性测试)
10
+
11
+ ## [0.3.1] - 2026-09-10
12
+
13
+ ### 变更
14
+
15
+ - feat(ts): 第四轮 JS→TS 迁移(5 个插件,server + client 全量)
16
+
17
+ ## [0.3.0] - 2026-09-07
18
+
19
+ ### 变更
20
+
21
+ - feat(observability): #155 错误上报统一——plugin_error 事件类型 + /errors API (#177)
22
+ - feat(observability): #155 资源监控扩展——新增 $DSH_HOME 目录总大小监控 (#176)
23
+ - feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
24
+ - chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
25
+
26
+ ## [0.2.0] - 2026-09-07
27
+
28
+ ### 变更
29
+
30
+ - feat(observability): #155 错误上报统一——plugin_error 事件类型 + /errors API (#177)
31
+ - feat(observability): #155 资源监控扩展——新增 $DSH_HOME 目录总大小监控 (#176)
32
+ - feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
33
+ - chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
34
+
5
35
  ## [0.1.7] - 2026-09-06
6
36
 
7
37
  ### 变更
package/README.md CHANGED
@@ -61,7 +61,7 @@ Server 端只读观察 DSH 生命周期事件并记录审计日志:
61
61
  ## 工作原理
62
62
 
63
63
  - **Server 端**(`lib/index.js`):`audit.js` 监听四类事件 → `store.js` 按会话分桶持久化;`git.js` 类型化提交(execFile);`review.js` + `diff.js` 规则引擎;`ai.js` 可选 AI 增强;`routes.js` 提供 `/observability/api` 路由(全部经 loopback 信任围栏)。
64
- - **Client 端**(`lib/client.js`):两个侧边栏页签(`dsh-my-observability:replay` 轨迹回放、`dsh-my-observability:git` Git 工具),样式走 DSH 语义 token,随 fiber 卸载无残留。
64
+ - **Client 端**(`lib/client.js`):两个侧边栏页签(`dsh-my-observability:replay` 轨迹回放、`dsh-my-observability:git` Git 工具),每个面板一个页签类型,经**宿主原生侧边栏扩展点**注册(`ctx.sidebarRightTabs` 页签类型 + `slots` 的 `sidebar.right.pane.tab` / `.title` 席位,issue #187 批 1),样式走 DSH 语义 token,随 fiber 卸载无残留。
65
65
 
66
66
  ## 安装
67
67
 
package/lib/ai.js CHANGED
@@ -10,67 +10,69 @@
10
10
  * 参考 dsh-task-reliability 的校验 agent 模式(agents.create + whenIdle
11
11
  * 超时 + dispose)。
12
12
  */
13
- import { randomUUID } from 'node:crypto'
14
- import { withTimeout, userMessage } from 'dsh-shared'
15
- import { REVIEW_TIMEOUT_MS } from './constants.js'
16
-
13
+ import { randomUUID } from 'node:crypto';
14
+ import { withTimeout, userMessage } from 'dsh-shared';
15
+ import { REVIEW_TIMEOUT_MS } from './constants.js';
17
16
  /** diff 送入 prompt 的最大长度(截断防 token 爆炸)。 */
18
- const AI_DIFF_MAX = 8000
19
-
17
+ const AI_DIFF_MAX = 8000;
20
18
  /** 运行 AI 审查(尽力而为;任何失败都降级为 failed 标记)。 */
21
19
  export async function runAiReview(ctx, diffText, report, timeoutMs = REVIEW_TIMEOUT_MS) {
22
- const agents = agentsServiceOf(ctx)
23
- if (agents === undefined) return { enabled: true, failed: true, note: 'agents service unavailable' }
24
- // CodeQL js/insecure-randomness 修复:sessionId 随机段改用 crypto.randomUUID()
25
- // (密码学安全随机,无需种子),保持 `obs-review-<ts>-<6位hex>` 格式兼容。
26
- const sessionId = `obs-review-${Date.now()}-${randomUUID().slice(0, 6)}`
27
- let handle
28
- try {
29
- handle = await agents.create({
30
- sessionId,
31
- meta: { origin: 'subagent', delegationDepth: 1 },
32
- agentOptions: {},
33
- })
34
- handle.agent.followup(userMessage(aiReviewPrompt(diffText, report)))
35
- await withTimeout(handle.agent.whenIdle(), timeoutMs)
36
- const parsed = parseAiConclusion(lastAssistantText(handle.agent.session))
37
- if (parsed === undefined) return { enabled: true, failed: true, note: 'AI 结论解析失败' }
38
- return { enabled: true, ...parsed }
39
- } catch (error) {
40
- return {
41
- enabled: true,
42
- failed: true,
43
- note: error instanceof Error ? error.message : String(error),
20
+ const agents = agentsServiceOf(ctx);
21
+ if (agents === undefined)
22
+ return { enabled: true, failed: true, note: 'agents service unavailable' };
23
+ // CodeQL js/insecure-randomness 修复:sessionId 随机段改用 crypto.randomUUID()
24
+ // (密码学安全随机,无需种子),保持 `obs-review-<ts>-<6位hex>` 格式兼容。
25
+ const sessionId = `obs-review-${Date.now()}-${randomUUID().slice(0, 6)}`;
26
+ let handle;
27
+ try {
28
+ handle = await agents.create({
29
+ sessionId,
30
+ meta: { origin: 'subagent', delegationDepth: 1 },
31
+ agentOptions: {},
32
+ });
33
+ handle.agent.followup(userMessage(aiReviewPrompt(diffText, report)));
34
+ await withTimeout(handle.agent.whenIdle(), timeoutMs);
35
+ const parsed = parseAiConclusion(lastAssistantText(handle.agent.session));
36
+ if (parsed === undefined)
37
+ return { enabled: true, failed: true, note: 'AI 结论解析失败' };
38
+ return { enabled: true, ...parsed };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ enabled: true,
43
+ failed: true,
44
+ note: error instanceof Error ? error.message : String(error),
45
+ };
46
+ }
47
+ finally {
48
+ await disposeHandle(handle);
44
49
  }
45
- } finally {
46
- await disposeHandle(handle)
47
- }
48
50
  }
49
-
50
51
  /** agents 服务读取(不可用返回 undefined)。 */
51
52
  function agentsServiceOf(ctx) {
52
- const agents = ctx.get ? ctx.get('agents') : undefined
53
- if (agents === undefined || agents === null || typeof agents.create !== 'function') return undefined
54
- return agents
53
+ const agents = ctx.get ? ctx.get('agents') : undefined;
54
+ if (agents === undefined || agents === null || typeof agents.create !== 'function')
55
+ return undefined;
56
+ return agents;
55
57
  }
56
-
57
58
  /** 释放审查 agent(尽力而为)。 */
58
59
  async function disposeHandle(handle) {
59
- if (handle === undefined) return
60
- try {
61
- await handle.dispose()
62
- } catch {
63
- // dispose is best-effort
64
- }
60
+ if (handle === undefined)
61
+ return;
62
+ try {
63
+ await handle.dispose();
64
+ }
65
+ catch {
66
+ // dispose is best-effort
67
+ }
65
68
  }
66
-
67
69
  /** 审查 prompt:规则引擎发现 + 截断的增量 diff,要求严格 JSON 输出。 */
68
70
  function aiReviewPrompt(diffText, report) {
69
- const diff = diffText.length > AI_DIFF_MAX ? `${diffText.slice(0, AI_DIFF_MAX)}\n…(diff 过长已截断)` : diffText
70
- const rules = report.issues
71
- .map((issue) => `- [${issue.severity}] ${issue.rule} ${issue.file}:${issue.line} ${issue.message}`)
72
- .join('\n')
73
- return `你是一个代码审查员。请审查以下提交前的增量 diff。规则引擎已给出参考问题,请结合 diff 给出总评与最重要的补充问题。
71
+ const diff = diffText.length > AI_DIFF_MAX ? `${diffText.slice(0, AI_DIFF_MAX)}\n…(diff 过长已截断)` : diffText;
72
+ const rules = report.issues
73
+ .map((issue) => `- [${issue.severity}] ${issue.rule} ${issue.file}:${issue.line} ${issue.message}`)
74
+ .join('\n');
75
+ return `你是一个代码审查员。请审查以下提交前的增量 diff。规则引擎已给出参考问题,请结合 diff 给出总评与最重要的补充问题。
74
76
 
75
77
  规则引擎发现:
76
78
  ${rules === '' ? '(无)' : rules}
@@ -81,79 +83,82 @@ ${diff}
81
83
  请严格只输出一个 JSON 对象(不要输出其他内容):
82
84
  {"verdict": "approve" 或 "changes", "summary": "50 字以内的总评", "topIssues": ["补充问题 1", "补充问题 2"]}
83
85
 
84
- 判断标准:存在阻塞性问题(密钥泄露、冲突标记、明显 bug)时 verdict 必须为 changes。`
86
+ 判断标准:存在阻塞性问题(密钥泄露、冲突标记、明显 bug)时 verdict 必须为 changes。`;
85
87
  }
86
-
87
88
  /** 从 agent 回复提取 JSON 结论(容忍 markdown 代码块包裹)。 */
88
89
  function parseAiConclusion(text) {
89
- const json = jsonTextOf(text)
90
- if (json === undefined) return undefined
91
- try {
92
- const parsed = JSON.parse(json)
93
- if (!isObject(parsed)) return undefined
94
- return {
95
- verdict: parsed.verdict === 'changes' ? 'changes' : 'approve',
96
- summary: stringOf(parsed.summary).slice(0, 500),
97
- topIssues: issuesOf(parsed.topIssues),
90
+ const json = jsonTextOf(text);
91
+ if (json === undefined)
92
+ return undefined;
93
+ try {
94
+ const parsed = JSON.parse(json);
95
+ if (!isObject(parsed))
96
+ return undefined;
97
+ return {
98
+ verdict: parsed.verdict === 'changes' ? 'changes' : 'approve',
99
+ summary: stringOf(parsed.summary).slice(0, 500),
100
+ topIssues: issuesOf(parsed.topIssues),
101
+ };
102
+ }
103
+ catch {
104
+ return undefined;
98
105
  }
99
- } catch {
100
- return undefined
101
- }
102
106
  }
103
-
104
107
  /** 提取 JSON 文本:裸 JSON 或 ```json 代码块。 */
105
108
  function jsonTextOf(text) {
106
- if (typeof text !== 'string' || text === '') return undefined
107
- const trimmed = text.trim()
108
- return trimmed.startsWith('{') ? trimmed : extractJsonBlock(trimmed)
109
+ if (typeof text !== 'string' || text === '')
110
+ return undefined;
111
+ const trimmed = text.trim();
112
+ return trimmed.startsWith('{') ? trimmed : extractJsonBlock(trimmed);
109
113
  }
110
-
111
114
  function isObject(value) {
112
- return value !== null && typeof value === 'object'
115
+ return value !== null && typeof value === 'object';
113
116
  }
114
-
115
117
  function stringOf(value) {
116
- return typeof value === 'string' ? value : ''
118
+ return typeof value === 'string' ? value : '';
117
119
  }
118
-
119
120
  /** topIssues 规整:字符串数组,上限 10 条。 */
120
121
  function issuesOf(raw) {
121
- return Array.isArray(raw) ? raw.filter((item) => typeof item === 'string').slice(0, 10) : []
122
+ return Array.isArray(raw) ? raw.filter((item) => typeof item === 'string').slice(0, 10) : [];
122
123
  }
123
-
124
124
  /** 从 ```json ... ``` 代码块提取 JSON 文本。 */
125
125
  function extractJsonBlock(text) {
126
- const match = /```(?:json)?\s*([\s\S]*?)```/.exec(text)
127
- return match !== null ? match[1].trim() : undefined
126
+ const match = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
127
+ return match !== null ? match[1].trim() : undefined;
128
128
  }
129
-
130
129
  /** 会话事件快照:新 API snapshotEvents() 优先,旧 API events 兜底(issue #165)。 */
131
130
  function sessionEvents(session) {
132
- return session?.snapshotEvents?.() ?? session?.events
131
+ const target = session;
132
+ return target?.snapshotEvents?.() ?? target?.events;
133
133
  }
134
-
135
134
  function lastAssistantText(session) {
136
- try {
137
- const events = sessionEvents(session)
138
- if (!Array.isArray(events)) return ''
139
- for (let i = events.length - 1; i >= 0; i--) {
140
- const text = assistantTextOf(events[i])
141
- if (text !== '') return text
135
+ try {
136
+ const events = sessionEvents(session);
137
+ if (!Array.isArray(events))
138
+ return '';
139
+ for (let i = events.length - 1; i >= 0; i--) {
140
+ const text = assistantTextOf(events[i]);
141
+ if (text !== '')
142
+ return text;
143
+ }
142
144
  }
143
- } catch {
144
- // best-effort
145
- }
146
- return ''
145
+ catch {
146
+ // best-effort
147
+ }
148
+ return '';
147
149
  }
148
-
149
150
  function assistantTextOf(event) {
150
- if (event?.type !== 'assistant/message') return ''
151
- const blocks = event.data?.message?.content
152
- if (!Array.isArray(blocks)) return ''
153
- return blocks
154
- .filter(
155
- (block) => block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string',
156
- )
157
- .map((block) => block.text)
158
- .join('\n')
151
+ const target = event;
152
+ if (target?.type !== 'assistant/message')
153
+ return '';
154
+ const blocks = target.data?.message?.content;
155
+ if (!Array.isArray(blocks))
156
+ return '';
157
+ return blocks
158
+ .filter((block) => block !== null &&
159
+ typeof block === 'object' &&
160
+ block.type === 'text' &&
161
+ typeof block.text === 'string')
162
+ .map((block) => block.text)
163
+ .join('\n');
159
164
  }