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/store.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-my-observability — audit event store.
|
|
3
|
+
*
|
|
4
|
+
* 事件审计日志的内存态 + 持久化:
|
|
5
|
+
* - 按会话隔离(bySession 分桶),查询/追加都限定在单个会话内;
|
|
6
|
+
* - 每会话事件上限(MAX_EVENTS_PER_SESSION,FIFO 淘汰),防无限膨胀;
|
|
7
|
+
* - 持久化 $DSH_HOME/observability/audit.json(防抖 500ms + 原子写
|
|
8
|
+
* tmp+rename + teardown flush),启动时异步加载(加载完成前的事件
|
|
9
|
+
* 缓冲在 pending,加载后回放),重启后完整恢复;
|
|
10
|
+
* - 全局事件上限(MAX_TOTAL_EVENTS),超限按会话轮转淘汰最旧会话事件。
|
|
11
|
+
*/
|
|
12
|
+
import { rename, writeFile, readFile, mkdir } from 'node:fs/promises'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import { join, dirname } from 'node:path'
|
|
15
|
+
|
|
16
|
+
export const MAX_EVENTS_PER_SESSION = 2000
|
|
17
|
+
export const MAX_TOTAL_EVENTS = 20000
|
|
18
|
+
|
|
19
|
+
/** 审计数据文件:$DSH_HOME/observability/audit.json(fallback ~/.dsh/…)。 */
|
|
20
|
+
export function stateFile() {
|
|
21
|
+
const home = process.env.DSH_HOME
|
|
22
|
+
const base = typeof home === 'string' && home !== '' ? home : homedir()
|
|
23
|
+
return join(base, 'observability', 'audit.json')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 初始空状态。 */
|
|
27
|
+
export function createState() {
|
|
28
|
+
return { version: 1, bySession: {} }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 创建审计存储:{ state, record, events, sessions, count, dispose }。
|
|
33
|
+
* record 在状态加载完成前缓冲(不丢事件);dispose 冲刷未落盘数据。
|
|
34
|
+
*/
|
|
35
|
+
export function createStore(ctx) {
|
|
36
|
+
const store = { state: createState() }
|
|
37
|
+
const handle = {
|
|
38
|
+
ctx,
|
|
39
|
+
file: stateFile(),
|
|
40
|
+
store,
|
|
41
|
+
pending: [],
|
|
42
|
+
ready: false,
|
|
43
|
+
persistTimer: null,
|
|
44
|
+
dirtyChain: Promise.resolve(),
|
|
45
|
+
}
|
|
46
|
+
store.record = (event) => record(handle, event)
|
|
47
|
+
store.events = (sessionId, type, limit) => eventsOf(handle, sessionId, type, limit)
|
|
48
|
+
store.sessions = () => sessionsOf(handle)
|
|
49
|
+
store.count = () => countOf(handle)
|
|
50
|
+
store.dispose = () => dispose(handle)
|
|
51
|
+
void readFile(handle.file, 'utf8')
|
|
52
|
+
.then((text) => onLoaded(handle, text))
|
|
53
|
+
.catch(() => onLoaded(handle, ''))
|
|
54
|
+
return store
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 追加一条审计事件(自动分配 id/时间戳);未就绪时缓冲。 */
|
|
58
|
+
function record(handle, event) {
|
|
59
|
+
const item = { id: nextId(handle), time: Date.now(), ...event }
|
|
60
|
+
if (!handle.ready) {
|
|
61
|
+
handle.pending.push(item)
|
|
62
|
+
return item
|
|
63
|
+
}
|
|
64
|
+
appendEvent(handle, item)
|
|
65
|
+
persistSoon(handle)
|
|
66
|
+
return item
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 查询某会话的事件(正序时间轴;type 可选过滤;limit 限制条数)。 */
|
|
70
|
+
function eventsOf(handle, sessionId, type, limit) {
|
|
71
|
+
const bucket = handle.store.state.bySession[sessionId]?.events
|
|
72
|
+
if (!Array.isArray(bucket)) return []
|
|
73
|
+
let list = bucket
|
|
74
|
+
if (type !== undefined && type !== null && type !== '') {
|
|
75
|
+
list = list.filter((event) => event.type === type)
|
|
76
|
+
}
|
|
77
|
+
const capped = typeof limit === 'number' && limit > 0 ? list.slice(-limit) : list
|
|
78
|
+
return capped.map((event) => ({ ...event }))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 有审计事件的会话列表(按最后活动时间倒序,含事件数)。 */
|
|
82
|
+
function sessionsOf(handle) {
|
|
83
|
+
const entries = Object.entries(handle.store.state.bySession)
|
|
84
|
+
const list = entries
|
|
85
|
+
.map(([sessionId, bucket]) => ({
|
|
86
|
+
sessionId,
|
|
87
|
+
count: bucket.events.length,
|
|
88
|
+
lastTime: bucket.events.length > 0 ? bucket.events[bucket.events.length - 1].time : 0,
|
|
89
|
+
}))
|
|
90
|
+
.filter((entry) => entry.count > 0)
|
|
91
|
+
list.sort((a, b) => b.lastTime - a.lastTime)
|
|
92
|
+
return list
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 全部会话事件总数(供状态展示/测试断言)。 */
|
|
96
|
+
function countOf(handle) {
|
|
97
|
+
let total = 0
|
|
98
|
+
for (const bucket of Object.values(handle.store.state.bySession)) total += bucket.events.length
|
|
99
|
+
return total
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 事件自增 id(跨会话单调)。 */
|
|
103
|
+
function nextId(handle) {
|
|
104
|
+
handle.seq = (handle.seq ?? 0) + 1
|
|
105
|
+
return handle.seq
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 追加事件到会话桶:FIFO 淘汰 + 全局上限轮转淘汰。 */
|
|
109
|
+
function appendEvent(handle, event) {
|
|
110
|
+
const state = handle.store.state
|
|
111
|
+
const bucket = state.bySession[event.sessionId] ?? (state.bySession[event.sessionId] = { events: [] })
|
|
112
|
+
bucket.events.push(event)
|
|
113
|
+
if (bucket.events.length > MAX_EVENTS_PER_SESSION) {
|
|
114
|
+
bucket.events.splice(0, bucket.events.length - MAX_EVENTS_PER_SESSION)
|
|
115
|
+
}
|
|
116
|
+
if (countOf(handle) > MAX_TOTAL_EVENTS) evictOldest(handle)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 全局超限:从最早活动的会话整桶淘汰,直到回到上限内。 */
|
|
120
|
+
function evictOldest(handle) {
|
|
121
|
+
const state = handle.store.state
|
|
122
|
+
const sessions = Object.entries(state.bySession)
|
|
123
|
+
.filter(([, bucket]) => bucket.events.length > 0)
|
|
124
|
+
.sort((a, b) => firstTimeOf(a[1]) - firstTimeOf(b[1]))
|
|
125
|
+
for (const [sessionId, bucket] of sessions) {
|
|
126
|
+
if (countOf(handle) <= MAX_TOTAL_EVENTS) break
|
|
127
|
+
delete state.bySession[sessionId]
|
|
128
|
+
void bucket
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function firstTimeOf(bucket) {
|
|
133
|
+
return bucket.events.length > 0 ? bucket.events[0].time : 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 状态加载完成:解析/规整 + 合并本进程已产生的事件 + 回放缓冲 + 落盘。 */
|
|
137
|
+
function onLoaded(handle, text) {
|
|
138
|
+
const parsed = parseLoaded(text)
|
|
139
|
+
if (parsed !== undefined) {
|
|
140
|
+
mergeCurrent(handle.store.state, parsed)
|
|
141
|
+
handle.store.state = parsed
|
|
142
|
+
}
|
|
143
|
+
handle.ready = true
|
|
144
|
+
const pending = handle.pending.splice(0)
|
|
145
|
+
for (const item of pending) appendEvent(handle, item)
|
|
146
|
+
if (pending.length > 0 || parsed !== undefined) persistSoon(handle)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 把当前 state 中已产生的事件合并进磁盘状态(防 dispose 回放后覆盖丢失)。 */
|
|
150
|
+
function mergeCurrent(current, parsed) {
|
|
151
|
+
for (const [sessionId, bucket] of Object.entries(current.bySession)) {
|
|
152
|
+
if (!Array.isArray(bucket.events) || bucket.events.length === 0) continue
|
|
153
|
+
const target = parsed.bySession[sessionId] ?? (parsed.bySession[sessionId] = { events: [] })
|
|
154
|
+
target.events.push(...bucket.events)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 解析已持久化的状态(结构不合法时回退空状态)。 */
|
|
159
|
+
function parseLoaded(text) {
|
|
160
|
+
if (text === undefined || text === null || text === '') return undefined
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(text)
|
|
163
|
+
if (!isValidRoot(parsed)) return undefined
|
|
164
|
+
const state = createState()
|
|
165
|
+
for (const [sessionId, bucket] of Object.entries(parsed.bySession)) {
|
|
166
|
+
const events = validEventsOf(bucket)
|
|
167
|
+
if (events.length > 0) state.bySession[sessionId] = { events }
|
|
168
|
+
}
|
|
169
|
+
return state
|
|
170
|
+
} catch {
|
|
171
|
+
return undefined
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 持久化根结构校验(bySession 必须为对象)。 */
|
|
176
|
+
function isValidRoot(parsed) {
|
|
177
|
+
return parsed !== null && typeof parsed === 'object' && typeof parsed.bySession === 'object'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 会话桶事件规整:过滤非法事件 + 截断到每会话上限。 */
|
|
181
|
+
function validEventsOf(bucket) {
|
|
182
|
+
return Array.isArray(bucket?.events)
|
|
183
|
+
? bucket.events.filter(isValidEvent).slice(-MAX_EVENTS_PER_SESSION)
|
|
184
|
+
: []
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 事件结构校验(时间/会话/类型为字符串与数字的合理形态)。 */
|
|
188
|
+
function isValidEvent(event) {
|
|
189
|
+
return event !== null && typeof event === 'object'
|
|
190
|
+
&& typeof event.time === 'number'
|
|
191
|
+
&& typeof event.sessionId === 'string'
|
|
192
|
+
&& typeof event.type === 'string'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 原子写当前状态(经 dirtyChain 串行化;自动建目录)。 */
|
|
196
|
+
function persistNow(handle) {
|
|
197
|
+
const snapshot = JSON.stringify(handle.store.state)
|
|
198
|
+
const tmp = `${handle.file}.tmp-${process.pid}`
|
|
199
|
+
handle.dirtyChain = handle.dirtyChain.then(async () => {
|
|
200
|
+
try {
|
|
201
|
+
await mkdir(dirname(handle.file), { recursive: true })
|
|
202
|
+
await writeFile(tmp, snapshot, 'utf8')
|
|
203
|
+
await rename(tmp, handle.file)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
handle.ctx.logger?.warn(`[dsh-my-observability] persist failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
206
|
+
}
|
|
207
|
+
}).catch(() => {})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 防抖(500ms)调度持久化。 */
|
|
211
|
+
function persistSoon(handle) {
|
|
212
|
+
if (handle.persistTimer !== null) return
|
|
213
|
+
handle.persistTimer = setTimeout(() => {
|
|
214
|
+
handle.persistTimer = null
|
|
215
|
+
persistNow(handle)
|
|
216
|
+
}, 500)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** 卸载冲刷:清定时器 + 回放未就绪缓冲 + 立即落盘。 */
|
|
220
|
+
function dispose(handle) {
|
|
221
|
+
if (handle.persistTimer !== null) {
|
|
222
|
+
clearTimeout(handle.persistTimer)
|
|
223
|
+
handle.persistTimer = null
|
|
224
|
+
}
|
|
225
|
+
if (!handle.ready) {
|
|
226
|
+
const pending = handle.pending.splice(0)
|
|
227
|
+
for (const item of pending) appendEvent(handle, item)
|
|
228
|
+
}
|
|
229
|
+
persistNow(handle)
|
|
230
|
+
void handle.dirtyChain
|
|
231
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-my-observability",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/baosfeng/my-dsh-plugins"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node scripts/build.mjs",
|
|
16
|
+
"test": "npx vitest run --coverage && cucumber-js test/features --import 'test/features/steps/*.mjs'"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"default": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./client": {
|
|
23
|
+
"default": "./lib/client.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"lib",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"assets"
|
|
34
|
+
],
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"platform": "web",
|
|
41
|
+
"inject": [
|
|
42
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"cordis": "^4.0.0-rc.8",
|
|
48
|
+
"react": "^18.2.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"cordis": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"react": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"license": "MIT"
|
|
59
|
+
}
|