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/audit.js
CHANGED
|
@@ -14,80 +14,77 @@
|
|
|
14
14
|
* 返回值,next() 同步返回流;async listener 会让消费方(vision-toolkit 等
|
|
15
15
|
* yield* 委托)拿到 Promise 而崩溃。tools/* 同理必须调用 next()。
|
|
16
16
|
*/
|
|
17
|
-
import { MAX_ARG_KEYS, MAX_TEXT_LEN } from './constants.js'
|
|
18
|
-
|
|
17
|
+
import { MAX_ARG_KEYS, MAX_TEXT_LEN } from './constants.js';
|
|
19
18
|
/**
|
|
20
19
|
* 采集的插件事件名(按插件前缀显式注册,不依赖 cordis 通配符行为)。
|
|
21
20
|
* 与 dsh-task-reliability/lib/emit.js 的 PLUGIN_EVENTS 保持一致;
|
|
22
21
|
* 未来推广到其他插件(dsh-my-guardian 等)在此追加。
|
|
23
22
|
*/
|
|
24
23
|
const PLUGIN_EVENT_NAMES = [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
]
|
|
31
|
-
|
|
24
|
+
'task-reliability/intervention',
|
|
25
|
+
'task-reliability/ask-decision',
|
|
26
|
+
'task-reliability/rescue',
|
|
27
|
+
'task-reliability/verify',
|
|
28
|
+
'task-reliability/resume',
|
|
29
|
+
];
|
|
32
30
|
/** 注册全部审计监听;返回 disposer 数组(全部经 ctx.on 注册)。 */
|
|
33
31
|
export function attachAuditListeners(ctx, record) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
return [
|
|
33
|
+
ctx.on('session/event', (session, event) => handleSessionEvent(session, event, record)),
|
|
34
|
+
ctx.on('agent/status', (payload) => handleStatus(payload, record)),
|
|
35
|
+
ctx.on('llm/stream', (options, next) => handleStream(options, next, record)),
|
|
36
|
+
ctx.on('tools/pre-execute', (exec, next) => handlePreExecute(exec, next, record)),
|
|
37
|
+
ctx.on('tools/execute', (exec, next) => handleExecute(exec, next, record)),
|
|
38
|
+
...PLUGIN_EVENT_NAMES.map((name) => ctx.on(name, (payload) => handlePluginEvent(name, payload, record))),
|
|
39
|
+
];
|
|
42
40
|
}
|
|
43
|
-
|
|
44
41
|
/** 插件事件名 → 插件名(前缀补 dsh-,如 task-reliability → dsh-task-reliability)。 */
|
|
45
42
|
function pluginNameOf(name) {
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
const prefix = name.split('/')[0];
|
|
44
|
+
return prefix === '' ? name : `dsh-${prefix}`;
|
|
48
45
|
}
|
|
49
|
-
|
|
50
46
|
/** 插件事件名 → 事件名(去掉前缀,如 task-reliability/intervention → intervention)。 */
|
|
51
47
|
function eventNameOf(name) {
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const parts = name.split('/');
|
|
49
|
+
return parts.length > 1 ? parts.slice(1).join('/') : name;
|
|
54
50
|
}
|
|
55
|
-
|
|
56
51
|
/** 插件事件 payload → 参数摘要(除 sessionId/action/reason 外的字段;字符串截断防膨胀)。 */
|
|
57
52
|
function summarizeParams(payload) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
const params = {};
|
|
54
|
+
for (const [key, value] of Object.entries(payload ?? {})) {
|
|
55
|
+
if (key === 'sessionId' || key === 'action' || key === 'reason')
|
|
56
|
+
continue;
|
|
57
|
+
if (typeof value === 'string')
|
|
58
|
+
params[key] = truncate(value);
|
|
59
|
+
else if (typeof value === 'number' || typeof value === 'boolean')
|
|
60
|
+
params[key] = value;
|
|
61
|
+
}
|
|
62
|
+
return params;
|
|
65
63
|
}
|
|
66
|
-
|
|
67
64
|
/**
|
|
68
65
|
* 插件事件 → plugin_event 审计事件(与现有事件同时间线/同过滤/同导出)。
|
|
69
66
|
* 无 sessionId 的插件事件不记录(与 agent_status 等一致)。
|
|
70
67
|
*/
|
|
71
68
|
function handlePluginEvent(name, payload, record) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
const sessionId = payload?.sessionId;
|
|
70
|
+
if (typeof sessionId !== 'string' || sessionId === '')
|
|
71
|
+
return;
|
|
72
|
+
const { type, data } = buildPluginEventData(name, payload);
|
|
73
|
+
record({ type, sessionId, data });
|
|
76
74
|
}
|
|
77
|
-
|
|
78
75
|
function buildPluginEventData(name, payload) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
const data = {
|
|
77
|
+
plugin: pluginNameOf(name),
|
|
78
|
+
event: eventNameOf(name),
|
|
79
|
+
action: truncate(String(payload?.action ?? '')),
|
|
80
|
+
reason: truncate(String(payload?.reason ?? '')),
|
|
81
|
+
params: summarizeParams(payload),
|
|
82
|
+
};
|
|
83
|
+
const error = typeof payload?.error === 'string' && payload.error !== '' ? truncate(payload.error) : null;
|
|
84
|
+
if (error !== null)
|
|
85
|
+
data.error = error;
|
|
86
|
+
return { type: error !== null ? 'plugin_error' : 'plugin_event', data };
|
|
89
87
|
}
|
|
90
|
-
|
|
91
88
|
/**
|
|
92
89
|
* session/event → user_message 事件(会话标题来源)。
|
|
93
90
|
* 轨迹回放面板需要"对话可读标题"而非 UUID:从每个会话真实用户的首条
|
|
@@ -95,163 +92,175 @@ function buildPluginEventData(name, payload) {
|
|
|
95
92
|
* title。不作为独立存储字段,走现有事件通路,重启后自然恢复。
|
|
96
93
|
*/
|
|
97
94
|
function handleSessionEvent(session, event, record) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
if (event === null || typeof event !== 'object' || event.type !== 'user/message')
|
|
96
|
+
return;
|
|
97
|
+
const message = event.data;
|
|
98
|
+
if (isPluginMessage(message))
|
|
99
|
+
return;
|
|
100
|
+
const sessionId = session?.id;
|
|
101
|
+
if (typeof sessionId !== 'string' || sessionId === '')
|
|
102
|
+
return;
|
|
103
|
+
const text = userTextOf(message);
|
|
104
|
+
if (text === '')
|
|
105
|
+
return;
|
|
106
|
+
record({ type: 'user_message', sessionId, data: { text: truncate(text) } });
|
|
106
107
|
}
|
|
107
|
-
|
|
108
108
|
/** 是否为插件注入的消息(非真实用户输入,不作为标题)。 */
|
|
109
109
|
function isPluginMessage(message) {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
const source = message?.source;
|
|
111
|
+
return source !== null && typeof source === 'object' && source.kind === 'plugin';
|
|
112
112
|
}
|
|
113
|
-
|
|
114
113
|
/** 从 user message 提取文本(content 中全部 text block 拼接)。 */
|
|
115
114
|
function userTextOf(message) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
if (message === null || typeof message !== 'object')
|
|
116
|
+
return '';
|
|
117
|
+
const content = message.content;
|
|
118
|
+
if (!Array.isArray(content))
|
|
119
|
+
return '';
|
|
120
|
+
const parts = [];
|
|
121
|
+
for (const block of content) {
|
|
122
|
+
if (block !== null &&
|
|
123
|
+
typeof block === 'object' &&
|
|
124
|
+
block.type === 'text' &&
|
|
125
|
+
typeof block.text === 'string') {
|
|
126
|
+
parts.push(block.text);
|
|
127
|
+
}
|
|
123
128
|
}
|
|
124
|
-
|
|
125
|
-
return parts.join(' ').trim()
|
|
129
|
+
return parts.join(' ').trim();
|
|
126
130
|
}
|
|
127
|
-
|
|
128
131
|
/** agent/status → agent_status 事件(含顶层/子代理标记)。 */
|
|
129
132
|
function handleStatus(payload, record) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
const agent = payload?.agent;
|
|
134
|
+
if (agent === null || typeof agent !== 'object')
|
|
135
|
+
return;
|
|
136
|
+
record({
|
|
137
|
+
type: 'agent_status',
|
|
138
|
+
sessionId: typeof agent.id === 'string' ? agent.id : '',
|
|
139
|
+
data: { status: String(payload?.status ?? ''), agentType: agentTypeOf(agent) },
|
|
140
|
+
});
|
|
137
141
|
}
|
|
138
|
-
|
|
139
142
|
/** llm/stream → 包装流(同步返回;无 sessionId 时原样透传)。 */
|
|
140
143
|
function handleStream(options, next, record) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
const sessionId = typeof options?.sessionId === 'string' ? options.sessionId : '';
|
|
145
|
+
if (sessionId === '')
|
|
146
|
+
return next();
|
|
147
|
+
const stream = next();
|
|
148
|
+
return wrapStream(sessionId, stream, record);
|
|
145
149
|
}
|
|
146
|
-
|
|
147
150
|
/** tools/pre-execute → tool_call 事件(透传 next)。 */
|
|
148
151
|
async function handlePreExecute(exec, next, record) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
if (exec !== null && typeof exec === 'object' && typeof exec.agent?.id === 'string') {
|
|
153
|
+
record({
|
|
154
|
+
type: 'tool_call',
|
|
155
|
+
sessionId: exec.agent.id,
|
|
156
|
+
data: { name: String(exec.name ?? ''), args: summarizeArguments(exec.arguments) },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return next();
|
|
157
160
|
}
|
|
158
|
-
|
|
159
161
|
/** tools/execute → tool_result 事件(透传 next 结果)。 */
|
|
160
162
|
async function handleExecute(exec, next, record) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
const sessionId = exec?.agent?.id;
|
|
164
|
+
const name = exec?.name;
|
|
165
|
+
if (typeof sessionId !== 'string' || sessionId === '')
|
|
166
|
+
return next();
|
|
167
|
+
const startedAt = Date.now();
|
|
168
|
+
const result = await next();
|
|
169
|
+
record({
|
|
170
|
+
type: 'tool_result',
|
|
171
|
+
sessionId,
|
|
172
|
+
data: {
|
|
173
|
+
name: String(name ?? ''),
|
|
174
|
+
ok: isToolOk(result),
|
|
175
|
+
ms: Date.now() - startedAt,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
return result;
|
|
176
179
|
}
|
|
177
|
-
|
|
178
180
|
/** 顶层/子代理标记(白名单化:任何子代理标记命中即 subagent)。 */
|
|
179
181
|
function agentTypeOf(agent) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
const header = agent.session?.header;
|
|
183
|
+
if (header === undefined || header === null)
|
|
184
|
+
return 'unknown';
|
|
185
|
+
if (header.origin === 'subagent')
|
|
186
|
+
return 'subagent';
|
|
187
|
+
if (typeof header.delegationDepth === 'number' && header.delegationDepth > 0)
|
|
188
|
+
return 'subagent';
|
|
189
|
+
return typeof agent.options?.subagentDepth === 'number' && agent.options.subagentDepth > 0 ? 'subagent' : 'top';
|
|
185
190
|
}
|
|
186
|
-
|
|
187
191
|
/** 工具结果 ok 判定:非对象视为成功;error 字段非空视为失败。 */
|
|
188
192
|
function isToolOk(result) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
if (result === null || typeof result !== 'object')
|
|
194
|
+
return true;
|
|
195
|
+
const error = result.error;
|
|
196
|
+
if (error === undefined || error === null)
|
|
197
|
+
return true;
|
|
198
|
+
return typeof error === 'object' && Object.keys(error).length === 0;
|
|
193
199
|
}
|
|
194
|
-
|
|
195
200
|
/** 参数摘要:键列表(上限)+ 主要文本参数摘要(上限),防审计膨胀。 */
|
|
196
201
|
function summarizeArguments(args) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
if (args === null || typeof args !== 'object')
|
|
203
|
+
return { keys: [] };
|
|
204
|
+
const keys = Object.keys(args).slice(0, MAX_ARG_KEYS);
|
|
205
|
+
const summary = textSummaryOf(args);
|
|
206
|
+
return summary === '' ? { keys } : { keys, summary };
|
|
201
207
|
}
|
|
202
|
-
|
|
203
208
|
/** 主要文本参数(command/message/content/question 等)的截断摘要。 */
|
|
204
209
|
function textSummaryOf(args) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
for (const key of ['command', 'message', 'content', 'question', 'description', 'prompt']) {
|
|
211
|
+
const value = args[key];
|
|
212
|
+
if (typeof value === 'string' && value !== '')
|
|
213
|
+
return truncate(value);
|
|
214
|
+
}
|
|
215
|
+
return '';
|
|
210
216
|
}
|
|
211
|
-
|
|
212
217
|
/** 截断长文本(保留首段,去换行)。 */
|
|
213
218
|
function truncate(text) {
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
const oneLine = text.split('\n')[0].trim();
|
|
220
|
+
return oneLine.length > MAX_TEXT_LEN ? `${oneLine.slice(0, MAX_TEXT_LEN)}…` : oneLine;
|
|
216
221
|
}
|
|
217
|
-
|
|
218
222
|
/** 包装 llm 流:透传全部 chunk,记录开始/结束/错误统计。 */
|
|
219
223
|
function wrapStream(sessionId, stream, record) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
let chunks = 0;
|
|
225
|
+
let chars = 0;
|
|
226
|
+
const startedAt = Date.now();
|
|
227
|
+
record({ type: 'llm_stream', sessionId, data: { phase: 'start' } });
|
|
228
|
+
return (async function* () {
|
|
229
|
+
try {
|
|
230
|
+
for await (const chunk of stream) {
|
|
231
|
+
chunks += 1;
|
|
232
|
+
if (chunk !== null && typeof chunk === 'object' && typeof chunk.text === 'string') {
|
|
233
|
+
chars += chunk.text.length;
|
|
234
|
+
}
|
|
235
|
+
yield chunk;
|
|
236
|
+
}
|
|
237
|
+
record({
|
|
238
|
+
type: 'llm_stream',
|
|
239
|
+
sessionId,
|
|
240
|
+
data: streamSummary('end', startedAt, chunks, chars),
|
|
241
|
+
});
|
|
230
242
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
type: 'llm_stream',
|
|
241
|
-
sessionId,
|
|
242
|
-
data: streamSummary('error', startedAt, chunks, chars, error),
|
|
243
|
-
})
|
|
244
|
-
throw error
|
|
245
|
-
}
|
|
246
|
-
})()
|
|
243
|
+
catch (error) {
|
|
244
|
+
record({
|
|
245
|
+
type: 'llm_stream',
|
|
246
|
+
sessionId,
|
|
247
|
+
data: streamSummary('error', startedAt, chunks, chars, error),
|
|
248
|
+
});
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
247
252
|
}
|
|
248
|
-
|
|
249
253
|
/** 流结束/错误统计(错误消息截断)。 */
|
|
250
254
|
function streamSummary(phase, startedAt, chunks, chars, error) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
const summary = {
|
|
256
|
+
phase,
|
|
257
|
+
chunks,
|
|
258
|
+
chars,
|
|
259
|
+
ms: Date.now() - startedAt,
|
|
260
|
+
};
|
|
261
|
+
if (error !== undefined) {
|
|
262
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
+
summary.message = message.length > MAX_TEXT_LEN ? `${message.slice(0, MAX_TEXT_LEN)}…` : message;
|
|
264
|
+
}
|
|
265
|
+
return summary;
|
|
257
266
|
}
|