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-view.js
CHANGED
|
@@ -11,263 +11,264 @@
|
|
|
11
11
|
* - 不读取 strings —— 涉及界面文案的默认值集中在此,client 如需 i18n 覆盖
|
|
12
12
|
* 通过参数传入。
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
14
|
/** 事件类型 → 中文标签(CSV 默认;client 可传 labels 覆盖)。非导出常量。 */
|
|
16
15
|
const DEFAULT_CSV_LABELS = Object.freeze({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
const MAX_STATS_TOP = 50
|
|
33
|
-
|
|
16
|
+
time: '时间',
|
|
17
|
+
type: '类型',
|
|
18
|
+
tool: '工具',
|
|
19
|
+
result: '结果',
|
|
20
|
+
typeMap: Object.freeze({
|
|
21
|
+
agent_status: 'agent 状态',
|
|
22
|
+
llm_stream: '模型流',
|
|
23
|
+
tool_call: '工具调用',
|
|
24
|
+
tool_result: '工具结果',
|
|
25
|
+
}),
|
|
26
|
+
ok: '成功',
|
|
27
|
+
fail: '失败',
|
|
28
|
+
error: '错误',
|
|
29
|
+
});
|
|
30
|
+
const MAX_STATS_TOP = 50;
|
|
34
31
|
/** 两位补零。 */
|
|
35
32
|
function pad2(n) {
|
|
36
|
-
|
|
33
|
+
return String(n).padStart(2, '0');
|
|
37
34
|
}
|
|
38
|
-
|
|
39
35
|
/** 毫秒时间戳 → `YYYY-MM-DD HH:MM:SS`(本地时区);非法输入返回空串。 */
|
|
40
36
|
export function formatTime(time) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
if (typeof time !== 'number' || !Number.isFinite(time))
|
|
38
|
+
return '';
|
|
39
|
+
const d = new Date(time);
|
|
40
|
+
if (Number.isNaN(d.getTime()))
|
|
41
|
+
return '';
|
|
42
|
+
const date = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
43
|
+
const clock = `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
44
|
+
return `${date} ${clock}`;
|
|
47
45
|
}
|
|
48
|
-
|
|
49
46
|
/** agent 状态事件的搜索片段。 */
|
|
50
47
|
function agentStatusParts(data) {
|
|
51
|
-
|
|
48
|
+
return [data.status, data.agentType].filter((part) => typeof part === 'string');
|
|
52
49
|
}
|
|
53
|
-
|
|
54
50
|
/** 模型流事件的搜索片段(含错误消息)。 */
|
|
55
51
|
function llmParts(data) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
const parts = [data.phase];
|
|
53
|
+
if (typeof data.message === 'string' && data.message !== '')
|
|
54
|
+
parts.push(data.message);
|
|
55
|
+
return parts;
|
|
59
56
|
}
|
|
60
|
-
|
|
61
57
|
/** 工具调用事件的搜索片段(工具名 + 参数键 + 参数摘要)。 */
|
|
62
58
|
function toolCallParts(data) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (typeof data.name === 'string')
|
|
61
|
+
parts.push(data.name);
|
|
62
|
+
if (Array.isArray(data.args?.keys))
|
|
63
|
+
parts.push(...data.args.keys);
|
|
64
|
+
if (typeof data.args?.summary === 'string' && data.args.summary !== '')
|
|
65
|
+
parts.push(data.args.summary);
|
|
66
|
+
return parts;
|
|
68
67
|
}
|
|
69
|
-
|
|
70
68
|
/** 工具结果事件的搜索片段(工具名 + 成败)。 */
|
|
71
69
|
function toolResultParts(data) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
const parts = [];
|
|
71
|
+
if (typeof data.name === 'string')
|
|
72
|
+
parts.push(data.name);
|
|
73
|
+
parts.push(data.ok === false ? '失败' : '成功');
|
|
74
|
+
return parts;
|
|
76
75
|
}
|
|
77
|
-
|
|
78
76
|
/** 插件事件(issue #154)的搜索片段(插件名/事件名/动作/原因/参数值)。 */
|
|
79
77
|
function pluginEventParts(data) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (typeof data.plugin === 'string')
|
|
80
|
+
parts.push(data.plugin);
|
|
81
|
+
if (typeof data.event === 'string')
|
|
82
|
+
parts.push(data.event);
|
|
83
|
+
if (typeof data.action === 'string')
|
|
84
|
+
parts.push(data.action);
|
|
85
|
+
if (typeof data.reason === 'string')
|
|
86
|
+
parts.push(data.reason);
|
|
87
|
+
if (data.params !== null && typeof data.params === 'object') {
|
|
88
|
+
for (const value of Object.values(data.params)) {
|
|
89
|
+
if (typeof value === 'string' && value !== '')
|
|
90
|
+
parts.push(value);
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
|
-
|
|
90
|
-
return parts
|
|
93
|
+
return parts;
|
|
91
94
|
}
|
|
92
|
-
|
|
93
95
|
/** 事件类型 → 搜索片段收集函数(查表消分支)。 */
|
|
94
96
|
const PARTS_COLLECTORS = {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
97
|
+
agent_status: agentStatusParts,
|
|
98
|
+
llm_stream: llmParts,
|
|
99
|
+
tool_call: toolCallParts,
|
|
100
|
+
tool_result: toolResultParts,
|
|
101
|
+
plugin_event: pluginEventParts,
|
|
102
|
+
};
|
|
102
103
|
/** 提取事件可用于关键词匹配的文本(工具名/参数摘要/错误信息/状态/阶段等)。 */
|
|
103
104
|
export function searchableText(event) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
const data = event && event.data ? event.data : {};
|
|
106
|
+
const parts = [event?.type, event?.sessionId];
|
|
107
|
+
const collector = PARTS_COLLECTORS[event?.type];
|
|
108
|
+
if (collector !== undefined)
|
|
109
|
+
parts.push(...collector(data));
|
|
110
|
+
return parts
|
|
111
|
+
.filter((part) => typeof part === 'string')
|
|
112
|
+
.join(' ')
|
|
113
|
+
.toLowerCase();
|
|
112
114
|
}
|
|
113
|
-
|
|
114
115
|
/** 事件是否命中关键词(不区分大小写;空关键词视为命中全部)。 */
|
|
115
116
|
export function matchesKeyword(event, keyword) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
const kw = String(keyword ?? '')
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
if (kw === '')
|
|
121
|
+
return true;
|
|
122
|
+
return searchableText(event).includes(kw);
|
|
121
123
|
}
|
|
122
|
-
|
|
123
124
|
/** 返回 `true` 表示事件具备失败语义(工具失败 / 模型流出错)。 */
|
|
124
125
|
function isFailEvent(event) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
if (event?.type === 'tool_result')
|
|
127
|
+
return event.data?.ok === false;
|
|
128
|
+
if (event?.type === 'llm_stream')
|
|
129
|
+
return event.data?.phase === 'error';
|
|
130
|
+
return false;
|
|
128
131
|
}
|
|
129
|
-
|
|
130
132
|
/** 归一化过滤条件(时间转为闭区间数值;空值透传)。 */
|
|
131
133
|
function normalizeCriteria(criteria) {
|
|
132
|
-
|
|
133
|
-
typeof criteria.
|
|
134
|
-
|
|
135
|
-
return { type: criteria.type ?? '', keyword: criteria.keyword ?? '', result: criteria.result ?? '', start, end }
|
|
134
|
+
const start = typeof criteria.timeStart === 'number' && Number.isFinite(criteria.timeStart) ? criteria.timeStart : undefined;
|
|
135
|
+
const end = typeof criteria.timeEnd === 'number' && Number.isFinite(criteria.timeEnd) ? criteria.timeEnd : undefined;
|
|
136
|
+
return { type: criteria.type ?? '', keyword: criteria.keyword ?? '', result: criteria.result ?? '', start, end };
|
|
136
137
|
}
|
|
137
|
-
|
|
138
138
|
/** 类型过滤('tool' 表示 tool_call + tool_result;'plugin' 表示 plugin_event)。 */
|
|
139
139
|
function passType(type, filterType) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
if (filterType === '')
|
|
141
|
+
return true;
|
|
142
|
+
if (filterType === 'tool')
|
|
143
|
+
return type === 'tool_call' || type === 'tool_result';
|
|
144
|
+
if (filterType === 'plugin')
|
|
145
|
+
return type === 'plugin_event';
|
|
146
|
+
return type === filterType;
|
|
144
147
|
}
|
|
145
|
-
|
|
146
148
|
/** 时间范围闭区间。 */
|
|
147
149
|
function passTime(time, start, end) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
if (start !== undefined && time < start)
|
|
151
|
+
return false;
|
|
152
|
+
if (end !== undefined && time > end)
|
|
153
|
+
return false;
|
|
154
|
+
return true;
|
|
151
155
|
}
|
|
152
|
-
|
|
153
156
|
/** 成功/失败过滤:只作用于有成败语义的事件,其余事件透传。 */
|
|
154
157
|
function passResult(event, result) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
if (result === '')
|
|
159
|
+
return true;
|
|
160
|
+
if (result === 'success')
|
|
161
|
+
return !isFailEvent(event);
|
|
162
|
+
if (result === 'fail')
|
|
163
|
+
return isFailEvent(event);
|
|
164
|
+
return true;
|
|
159
165
|
}
|
|
160
|
-
|
|
161
166
|
/** 组合过滤:类型(tool 表示 tool_call+tool_result)+ 时间范围 + 成功/失败 + 关键词。
|
|
162
167
|
* criteria: { type, timeStart, timeEnd, result, keyword } */
|
|
163
168
|
export function applyAuditFilter(events, criteria = {}) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
passResult(event, ctx.result) &&
|
|
170
|
-
matchesKeyword(event, ctx.keyword),
|
|
171
|
-
)
|
|
169
|
+
const ctx = normalizeCriteria(criteria);
|
|
170
|
+
return (events ?? []).filter((event) => passType(event.type, ctx.type) &&
|
|
171
|
+
passTime(event.time, ctx.start, ctx.end) &&
|
|
172
|
+
passResult(event, ctx.result) &&
|
|
173
|
+
matchesKeyword(event, ctx.keyword));
|
|
172
174
|
}
|
|
173
|
-
|
|
174
175
|
/** CSV 单元格转义:含逗号/引号/换行时用双引号包裹并转义内嵌引号。 */
|
|
175
176
|
export function csvCell(value) {
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
const s = value === null || value === undefined ? '' : String(value);
|
|
178
|
+
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
178
179
|
}
|
|
179
|
-
|
|
180
180
|
/** 事件的工具名(仅 tool_call/tool_result;否则空)。 */
|
|
181
181
|
export function toolNameOf(event) {
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
if (event?.type === 'tool_call' || event?.type === 'tool_result')
|
|
183
|
+
return String(event.data?.name ?? '');
|
|
184
|
+
return '';
|
|
184
185
|
}
|
|
185
|
-
|
|
186
186
|
/** 事件的结果摘要(成功/失败/错误;其余空)。 */
|
|
187
187
|
export function resultTextOf(event, labels = DEFAULT_CSV_LABELS) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
if (event?.type === 'tool_result')
|
|
189
|
+
return event.data?.ok === false ? labels.fail : labels.ok;
|
|
190
|
+
if (event?.type === 'llm_stream' && event.data?.phase === 'error')
|
|
191
|
+
return labels.error;
|
|
192
|
+
return '';
|
|
191
193
|
}
|
|
192
|
-
|
|
193
194
|
/** 生成 CSV 摘要(表头:时间/类型/工具/结果)。labels 可覆盖默认中文。
|
|
194
195
|
* 返回不含换行结尾符的 CSV 文本。 */
|
|
195
196
|
export function auditToCsv(events, labels = DEFAULT_CSV_LABELS) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
197
|
+
const typeMap = labels.typeMap ?? {};
|
|
198
|
+
const header = [labels.time, labels.type, labels.tool, labels.result];
|
|
199
|
+
const lines = [header.map(csvCell).join(',')];
|
|
200
|
+
for (const event of (events ?? [])) {
|
|
201
|
+
const typeLabel = typeMap[event.type] ?? String(event.type);
|
|
202
|
+
const row = [formatTime(event.time), typeLabel, toolNameOf(event), resultTextOf(event, labels)];
|
|
203
|
+
lines.push(row.map(csvCell).join(','));
|
|
204
|
+
}
|
|
205
|
+
return lines.join('\n');
|
|
205
206
|
}
|
|
206
|
-
|
|
207
207
|
/** 生成 JSON 完整数据(缩进默认 2)。 */
|
|
208
208
|
export function auditToJson(events, space = 2) {
|
|
209
|
-
|
|
209
|
+
return JSON.stringify(events ?? [], null, space);
|
|
210
210
|
}
|
|
211
|
-
|
|
212
211
|
/** 事件是否为工具类(tool_call / tool_result)。 */
|
|
213
212
|
function isToolEvent(event) {
|
|
214
|
-
|
|
213
|
+
return event?.type === 'tool_call' || event?.type === 'tool_result';
|
|
215
214
|
}
|
|
216
|
-
|
|
217
215
|
/** 把单条工具事件计入聚合(调用次数 / 失败次数)。 */
|
|
218
216
|
function bumpTool(byTool, event, name) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
217
|
+
const entry = byTool.get(name) ?? { tool: name, calls: 0, fails: 0 };
|
|
218
|
+
if (event.type === 'tool_call')
|
|
219
|
+
entry.calls += 1;
|
|
220
|
+
if (event.type === 'tool_result' && event.data?.ok === false)
|
|
221
|
+
entry.fails += 1;
|
|
222
|
+
byTool.set(name, entry);
|
|
223
223
|
}
|
|
224
|
-
|
|
225
224
|
/** 按工具名聚合调用次数与失败次数。 */
|
|
226
225
|
function aggregateToolStats(events) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
const byTool = new Map();
|
|
227
|
+
for (const event of (events ?? [])) {
|
|
228
|
+
if (!isToolEvent(event))
|
|
229
|
+
continue;
|
|
230
|
+
const name = String(event.data?.name ?? '');
|
|
231
|
+
if (name === '')
|
|
232
|
+
continue;
|
|
233
|
+
bumpTool(byTool, event, name);
|
|
234
|
+
}
|
|
235
|
+
return byTool;
|
|
235
236
|
}
|
|
236
|
-
|
|
237
237
|
/** 聚合结果 → 排序 + 失败率列表。 */
|
|
238
238
|
function rankTools(byTool) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
return [...byTool.values()]
|
|
240
|
+
.map((entry) => ({ ...entry, failRate: entry.calls > 0 ? entry.fails / entry.calls : 0 }))
|
|
241
|
+
.sort((a, b) => b.calls - a.calls || b.fails - a.fails || a.tool.localeCompare(b.tool));
|
|
242
242
|
}
|
|
243
|
-
|
|
244
243
|
/** 工具调用统计:每个工具调用次数 + 失败率(topN 截断,默认 5)。
|
|
245
244
|
* 返回 [{ tool, calls, fails, failRate }] 按调用次数降序。 */
|
|
246
245
|
export function computeToolStats(events, topN = 5) {
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
const n = typeof topN === 'number' && topN > 0 ? Math.min(topN, MAX_STATS_TOP) : 5;
|
|
247
|
+
return rankTools(aggregateToolStats(events)).slice(0, n);
|
|
249
248
|
}
|
|
250
|
-
|
|
251
249
|
/** 把 text 按 keyword 切成 [ { text, hit } ] 分段(用于命中关键词高亮)。
|
|
252
250
|
* 空关键词返回整段未命中。 */
|
|
253
251
|
export function highlightSegments(text, keyword) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
252
|
+
const raw = String(text ?? '');
|
|
253
|
+
const kw = String(keyword ?? '')
|
|
254
|
+
.trim()
|
|
255
|
+
.toLowerCase();
|
|
256
|
+
if (kw === '')
|
|
257
|
+
return [{ text: raw, hit: false }];
|
|
258
|
+
const lower = raw.toLowerCase();
|
|
259
|
+
const out = [];
|
|
260
|
+
let cursor = 0;
|
|
261
|
+
for (;;) {
|
|
262
|
+
const idx = lower.indexOf(kw, cursor);
|
|
263
|
+
if (idx === -1) {
|
|
264
|
+
if (cursor < raw.length)
|
|
265
|
+
out.push({ text: raw.slice(cursor), hit: false });
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
if (idx > cursor)
|
|
269
|
+
out.push({ text: raw.slice(cursor, idx), hit: false });
|
|
270
|
+
out.push({ text: raw.slice(idx, idx + kw.length), hit: true });
|
|
271
|
+
cursor = idx + kw.length;
|
|
267
272
|
}
|
|
268
|
-
|
|
269
|
-
out.push({ text: raw.slice(idx, idx + kw.length), hit: true })
|
|
270
|
-
cursor = idx + kw.length
|
|
271
|
-
}
|
|
272
|
-
return out.length === 0 ? [{ text: raw, hit: false }] : out
|
|
273
|
+
return out.length === 0 ? [{ text: raw, hit: false }] : out;
|
|
273
274
|
}
|