dsh-tool-stats 0.2.1 → 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/lib/analyzer.d.ts +9 -1
- package/lib/analyzer.js +75 -0
- package/lib/client/locales.d.ts +10 -0
- package/lib/client/locales.js +20 -0
- package/lib/client/view.d.ts +0 -3
- package/lib/client/view.js +64 -26
- package/lib/index.d.ts +7 -0
- package/lib/index.js +4 -0
- package/lib/routes.d.ts +3 -0
- package/lib/routes.js +51 -3
- package/lib/stats.d.ts +9 -1
- package/lib/stats.js +33 -1
- package/lib/stats.web.js +168 -25
- package/lib/stats.web.js.map +2 -2
- package/lib/store.d.ts +2 -0
- package/lib/store.js +15 -1
- package/lib/types.d.ts +34 -0
- package/package.json +1 -1
package/lib/analyzer.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import type { ToolCall, ToolCounter, DeadTool, FailingTool, Recommendation } from './types.js';
|
|
1
|
+
import type { ToolCall, ToolCounter, DeadTool, FailingTool, Recommendation, DailyToolTrend, ToolDetail, Alert, ToolStatsOverview } from './types.js';
|
|
2
2
|
export declare function aggregate(calls: ToolCall[]): Map<string, ToolCounter>;
|
|
3
3
|
export declare function findDeadTools(configuredTools: string[], calls: ToolCall[], threshold?: number): DeadTool[];
|
|
4
4
|
export declare function findFailingTools(calls: ToolCall[], threshold?: number): FailingTool[];
|
|
5
5
|
export declare function recommend(configuredTools: string[], calls: ToolCall[], deadThreshold?: number, estimatedTokensPerTool?: number): Recommendation[];
|
|
6
|
+
/** Build daily trend for a specific tool. */
|
|
7
|
+
export declare function dailyTrend(calls: ToolCall[], toolName: string): DailyToolTrend[];
|
|
8
|
+
/** Detailed view for a single tool. */
|
|
9
|
+
export declare function toolDetail(calls: ToolCall[], toolName: string): ToolDetail | null;
|
|
10
|
+
/** Generate alerts for failing tools. */
|
|
11
|
+
export declare function generateAlerts(calls: ToolCall[], threshold?: number): Alert[];
|
|
12
|
+
/** Compute overview stats. */
|
|
13
|
+
export declare function overview(calls: ToolCall[]): ToolStatsOverview;
|
package/lib/analyzer.js
CHANGED
|
@@ -90,3 +90,78 @@ export function recommend(configuredTools, calls, deadThreshold = 10, estimatedT
|
|
|
90
90
|
}
|
|
91
91
|
return recs;
|
|
92
92
|
}
|
|
93
|
+
/** Build daily trend for a specific tool. */
|
|
94
|
+
export function dailyTrend(calls, toolName) {
|
|
95
|
+
const toolCalls = calls.filter((c) => c.tool === toolName);
|
|
96
|
+
const byDay = new Map();
|
|
97
|
+
for (const c of toolCalls) {
|
|
98
|
+
const day = new Date(c.timestamp).toISOString().slice(0, 10);
|
|
99
|
+
const d = byDay.get(day) ?? { invocations: 0, failures: 0, totalLatency: 0 };
|
|
100
|
+
d.invocations++;
|
|
101
|
+
if (!c.success)
|
|
102
|
+
d.failures++;
|
|
103
|
+
d.totalLatency += c.latencyMs;
|
|
104
|
+
byDay.set(day, d);
|
|
105
|
+
}
|
|
106
|
+
return [...byDay.entries()]
|
|
107
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
108
|
+
.map(([date, d]) => ({
|
|
109
|
+
date,
|
|
110
|
+
invocations: d.invocations,
|
|
111
|
+
failures: d.failures,
|
|
112
|
+
avgLatencyMs: Math.round(d.totalLatency / d.invocations),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
/** Detailed view for a single tool. */
|
|
116
|
+
export function toolDetail(calls, toolName) {
|
|
117
|
+
const counters = aggregate(calls);
|
|
118
|
+
const counter = counters.get(toolName);
|
|
119
|
+
if (!counter)
|
|
120
|
+
return null;
|
|
121
|
+
const toolCalls = calls.filter((c) => c.tool === toolName);
|
|
122
|
+
const recentCalls = toolCalls.sort((a, b) => b.timestamp - a.timestamp).slice(0, 50);
|
|
123
|
+
const trend = dailyTrend(calls, toolName);
|
|
124
|
+
// Error breakdown
|
|
125
|
+
const errorCounts = new Map();
|
|
126
|
+
for (const c of toolCalls) {
|
|
127
|
+
if (!c.success && c.errorMessage) {
|
|
128
|
+
const msg = c.errorMessage.slice(0, 100);
|
|
129
|
+
errorCounts.set(msg, (errorCounts.get(msg) ?? 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const errorBreakdown = [...errorCounts.entries()]
|
|
133
|
+
.map(([message, count]) => ({ message, count }))
|
|
134
|
+
.sort((a, b) => b.count - a.count);
|
|
135
|
+
return { counter, recentCalls, trend, errorBreakdown };
|
|
136
|
+
}
|
|
137
|
+
/** Generate alerts for failing tools. */
|
|
138
|
+
export function generateAlerts(calls, threshold = 0.5) {
|
|
139
|
+
const alerts = [];
|
|
140
|
+
const counters = aggregate(calls);
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
for (const c of counters.values()) {
|
|
143
|
+
if (c.invocations >= 5 && c.failureRate >= threshold) {
|
|
144
|
+
alerts.push({
|
|
145
|
+
level: c.failureRate >= 0.8 ? 'error' : 'warn',
|
|
146
|
+
tool: c.tool,
|
|
147
|
+
message: `Failure rate ${Math.round(c.failureRate * 100)}% (${c.failures}/${c.invocations})`,
|
|
148
|
+
timestamp: now,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return alerts;
|
|
153
|
+
}
|
|
154
|
+
/** Compute overview stats. */
|
|
155
|
+
export function overview(calls) {
|
|
156
|
+
const totalCalls = calls.length;
|
|
157
|
+
const totalFailures = calls.filter((c) => !c.success).length;
|
|
158
|
+
const sessions = new Set(calls.map((c) => c.sessionId));
|
|
159
|
+
const totalLatency = calls.reduce((s, c) => s + c.latencyMs, 0);
|
|
160
|
+
return {
|
|
161
|
+
totalCalls,
|
|
162
|
+
totalFailures,
|
|
163
|
+
overallFailureRate: totalCalls > 0 ? totalFailures / totalCalls : 0,
|
|
164
|
+
activeSessions: sessions.size,
|
|
165
|
+
avgLatencyMs: totalCalls > 0 ? Math.round(totalLatency / totalCalls) : 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export declare const zh: {
|
|
|
24
24
|
loading: string;
|
|
25
25
|
failed: string;
|
|
26
26
|
retry: string;
|
|
27
|
+
totalCalls: string;
|
|
28
|
+
totalFailures: string;
|
|
29
|
+
sessions: string;
|
|
30
|
+
avgLatency: string;
|
|
31
|
+
export: string;
|
|
32
|
+
clear: string;
|
|
33
|
+
confirmClear: string;
|
|
34
|
+
errorBreakdown: string;
|
|
35
|
+
recentCalls: string;
|
|
36
|
+
sessionsUnused: string;
|
|
27
37
|
};
|
|
28
38
|
/** English dictionary, checked complete against the zh key set. */
|
|
29
39
|
export declare const en: typeof zh;
|
package/lib/client/locales.js
CHANGED
|
@@ -24,6 +24,16 @@ export const zh = {
|
|
|
24
24
|
'loading': '正在加载…',
|
|
25
25
|
'failed': '加载失败',
|
|
26
26
|
'retry': '重试',
|
|
27
|
+
'totalCalls': '总调用',
|
|
28
|
+
'totalFailures': '总失败',
|
|
29
|
+
'sessions': '会话数',
|
|
30
|
+
'avgLatency': '平均延迟',
|
|
31
|
+
'export': '导出 CSV',
|
|
32
|
+
'clear': '清空',
|
|
33
|
+
'confirmClear': '确定清空所有记录?',
|
|
34
|
+
'errorBreakdown': '错误分类',
|
|
35
|
+
'recentCalls': '最近调用',
|
|
36
|
+
'sessionsUnused': '会话未用',
|
|
27
37
|
};
|
|
28
38
|
/** English dictionary, checked complete against the zh key set. */
|
|
29
39
|
export const en = {
|
|
@@ -40,4 +50,14 @@ export const en = {
|
|
|
40
50
|
'loading': 'Loading…',
|
|
41
51
|
'failed': 'Failed to load',
|
|
42
52
|
'retry': 'Retry',
|
|
53
|
+
'totalCalls': 'Total Calls',
|
|
54
|
+
'totalFailures': 'Total Failures',
|
|
55
|
+
'sessions': 'Sessions',
|
|
56
|
+
'avgLatency': 'Avg Latency',
|
|
57
|
+
'export': 'Export CSV',
|
|
58
|
+
'clear': 'Clear',
|
|
59
|
+
'confirmClear': 'Clear all recorded data?',
|
|
60
|
+
'errorBreakdown': 'Error Breakdown',
|
|
61
|
+
'recentCalls': 'Recent Calls',
|
|
62
|
+
'sessionsUnused': 'sessions unused',
|
|
43
63
|
};
|
package/lib/client/view.d.ts
CHANGED
|
@@ -9,17 +9,14 @@
|
|
|
9
9
|
* @module client/view
|
|
10
10
|
*/
|
|
11
11
|
import type { PanelPayload } from '../types.js';
|
|
12
|
-
/** The translate seat the renderer binds from this plugin's locale namespace. */
|
|
13
12
|
export type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
14
13
|
export interface PanelProps {
|
|
15
|
-
/** Bound translate function for this plugin's namespace. */
|
|
16
14
|
t: Translate;
|
|
17
15
|
}
|
|
18
16
|
interface PanelState {
|
|
19
17
|
payload: PanelPayload | null;
|
|
20
18
|
error: string | null;
|
|
21
19
|
}
|
|
22
|
-
/** Fetch the host panel payload; `reload` re-runs the request. */
|
|
23
20
|
export declare function usePanel(): PanelState & {
|
|
24
21
|
reload: () => void;
|
|
25
22
|
};
|
package/lib/client/view.js
CHANGED
|
@@ -10,49 +10,87 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* @module client/view
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback, useEffect, useState } from 'react';
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
const PANEL_PATH = '/api/stats.panel';
|
|
14
|
+
const DETAIL_PATH = '/api/stats.detail';
|
|
15
|
+
const EXPORT_PATH = '/api/stats.export';
|
|
16
|
+
const CLEAR_PATH = '/api/stats.clear';
|
|
17
|
+
const wrap = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 820, fontFamily: 'inherit' };
|
|
16
18
|
const head = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' };
|
|
17
19
|
const muted = { fontSize: 12, opacity: 0.75 };
|
|
18
20
|
const table = { borderCollapse: 'collapse', width: '100%' };
|
|
19
21
|
const th = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' };
|
|
20
22
|
const td = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' };
|
|
21
23
|
const list = { margin: 0, paddingLeft: 18, fontSize: 13 };
|
|
22
|
-
|
|
24
|
+
const card = { padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.2)', fontSize: 13 };
|
|
25
|
+
const statBox = { ...card, flex: 1, minWidth: 100, textAlign: 'center' };
|
|
26
|
+
const statRow = { display: 'flex', gap: 12, flexWrap: 'wrap' };
|
|
27
|
+
const btn = { fontSize: 12, cursor: 'pointer', padding: '3px 10px', borderRadius: 4, border: '0.5px solid rgba(128,128,128,0.4)' };
|
|
28
|
+
const dangerBtn = { ...btn, color: '#e55', borderColor: '#e55' };
|
|
29
|
+
const alertCard = { ...card, display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 };
|
|
30
|
+
const preBlock = { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 11, maxHeight: 150, overflow: 'auto', padding: 6, borderRadius: 4, background: 'rgba(128,128,128,0.06)' };
|
|
31
|
+
const clickRow = { cursor: 'pointer' };
|
|
23
32
|
export function usePanel() {
|
|
24
33
|
const [state, setState] = useState({ payload: null, error: null });
|
|
25
34
|
const [tick, setTick] = useState(0);
|
|
26
|
-
const reload = useCallback(() => setTick((
|
|
35
|
+
const reload = useCallback(() => setTick((v) => v + 1), []);
|
|
27
36
|
useEffect(() => {
|
|
28
|
-
const
|
|
29
|
-
setState((
|
|
30
|
-
fetch(PANEL_PATH, { signal:
|
|
31
|
-
.then(async (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
setState({ payload, error: null });
|
|
39
|
-
})
|
|
40
|
-
.catch((cause) => {
|
|
41
|
-
if (controller.signal.aborted)
|
|
42
|
-
return;
|
|
43
|
-
setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) });
|
|
44
|
-
});
|
|
45
|
-
return () => controller.abort();
|
|
37
|
+
const c = new AbortController();
|
|
38
|
+
setState((p) => ({ ...p, error: null }));
|
|
39
|
+
fetch(PANEL_PATH, { signal: c.signal })
|
|
40
|
+
.then(async (r) => { if (!r.ok)
|
|
41
|
+
throw new Error(String(r.status)); return r.json(); })
|
|
42
|
+
.then((payload) => { if (!c.signal.aborted)
|
|
43
|
+
setState({ payload, error: null }); })
|
|
44
|
+
.catch((e) => { if (!c.signal.aborted)
|
|
45
|
+
setState({ payload: null, error: e instanceof Error ? e.message : String(e) }); });
|
|
46
|
+
return () => c.abort();
|
|
46
47
|
}, [tick]);
|
|
47
48
|
return { ...state, reload };
|
|
48
49
|
}
|
|
50
|
+
// --- Alert banner ---
|
|
51
|
+
function AlertBanner({ alerts, t }) {
|
|
52
|
+
if (alerts.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return (_jsx("div", { children: alerts.map((a, i) => (_jsxs("div", { style: { ...alertCard, borderColor: a.level === 'error' ? '#e55' : '#ed0', background: a.level === 'error' ? 'rgba(255,80,80,0.06)' : 'rgba(255,200,0,0.06)' }, children: [_jsx("span", { style: { fontSize: 16 }, children: a.level === 'error' ? '🔴' : '🟡' }), _jsxs("span", { children: [_jsx("strong", { children: a.tool }), ": ", a.message] })] }, i))) }));
|
|
55
|
+
}
|
|
56
|
+
// --- Overview stats ---
|
|
57
|
+
function Overview({ payload, t }) {
|
|
58
|
+
const o = payload.overview;
|
|
59
|
+
return (_jsxs("div", { style: statRow, children: [_jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700 }, children: o.totalCalls }), _jsx("div", { style: muted, children: t('totalCalls') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700, color: o.totalFailures > 0 ? '#e55' : undefined }, children: o.totalFailures }), _jsx("div", { style: muted, children: t('totalFailures') })] }), _jsxs("div", { style: statBox, children: [_jsxs("div", { style: { fontSize: 18, fontWeight: 700 }, children: [Math.round(o.overallFailureRate * 100), "%"] }), _jsx("div", { style: muted, children: t('failRate') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700 }, children: o.activeSessions }), _jsx("div", { style: muted, children: t('sessions') })] }), _jsxs("div", { style: statBox, children: [_jsxs("div", { style: { fontSize: 18, fontWeight: 700 }, children: [o.avgLatencyMs, "ms"] }), _jsx("div", { style: muted, children: t('avgLatency') })] })] }));
|
|
60
|
+
}
|
|
61
|
+
// --- Tool detail expansion ---
|
|
62
|
+
function ToolDetailRow({ tool, t }) {
|
|
63
|
+
const [detail, setDetail] = useState(null);
|
|
64
|
+
const [loading, setLoading] = useState(false);
|
|
65
|
+
const toggle = useCallback(async () => {
|
|
66
|
+
if (detail) {
|
|
67
|
+
setDetail(null);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
setLoading(true);
|
|
71
|
+
try {
|
|
72
|
+
const r = await fetch(`${DETAIL_PATH}?tool=${encodeURIComponent(tool)}`);
|
|
73
|
+
if (r.ok)
|
|
74
|
+
setDetail(await r.json());
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
setLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}, [detail, tool]);
|
|
80
|
+
return (_jsxs(_Fragment, { children: [_jsx("tr", { style: clickRow, onClick: toggle, children: _jsx("td", { style: td, children: loading ? '…' : detail ? '▼' : '▶' }) }), detail && (_jsx("tr", { children: _jsx("td", { colSpan: 6, style: { padding: '4px 0' }, children: _jsxs("div", { style: card, children: [detail.errorBreakdown.length > 0 && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("strong", { style: { fontSize: 12 }, children: t('errorBreakdown') }), detail.errorBreakdown.map((e, i) => (_jsxs("div", { style: { ...preBlock, marginTop: 4 }, children: [_jsxs("span", { style: { color: '#e55', fontWeight: 600 }, children: ["\u00D7", e.count] }), " ", e.message] }, i)))] })), _jsx("strong", { style: { fontSize: 12 }, children: t('recentCalls') }), _jsx("table", { style: { ...table, marginTop: 4 }, children: _jsx("tbody", { children: detail.recentCalls.slice(0, 10).map((c, i) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: c.success ? '✓' : '✗' }), _jsxs("td", { style: td, children: [c.latencyMs, "ms"] }), _jsx("td", { style: td, children: new Date(c.timestamp).toLocaleTimeString() }), _jsx("td", { style: { ...td, fontSize: 10, opacity: 0.7 }, children: c.errorMessage?.slice(0, 60) })] }, i))) }) })] }) }) }))] }));
|
|
81
|
+
}
|
|
49
82
|
export function ToolStatsPanel({ t }) {
|
|
50
83
|
const { payload, error, reload } = usePanel();
|
|
51
|
-
const
|
|
84
|
+
const handleClear = useCallback(() => {
|
|
85
|
+
if (!confirm(t('confirmClear')))
|
|
86
|
+
return;
|
|
87
|
+
fetch(CLEAR_PATH, { method: 'POST' }).then(() => reload()).catch(() => { });
|
|
88
|
+
}, [t, reload]);
|
|
89
|
+
const header = (_jsxs("header", { style: head, children: [_jsx("strong", { style: { fontSize: 13 }, children: t('title') }), _jsx("span", { style: { flex: 1 } }), _jsx("a", { href: EXPORT_PATH, download: true, style: { ...btn, textDecoration: 'none', color: 'inherit' }, children: t('export') }), _jsx("button", { type: "button", style: dangerBtn, onClick: handleClear, children: t('clear') }), _jsx("button", { type: "button", style: btn, onClick: reload, children: t('refresh') })] }));
|
|
52
90
|
if (error !== null) {
|
|
53
|
-
return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { alignSelf: 'flex-start'
|
|
91
|
+
return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { ...btn, alignSelf: 'flex-start' }, children: t('retry') })] }));
|
|
54
92
|
}
|
|
55
93
|
if (payload === null)
|
|
56
94
|
return _jsx("p", { style: muted, "aria-live": "polite", children: t('loading') });
|
|
57
|
-
return (_jsxs("div", { style: wrap, children: [header, payload.tools.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: t('tool') }), _jsx("th", { style: th, children: t('calls') }), _jsx("th", { style: th, children: t('failRate') }), _jsx("th", { style: th, children: "p50" }), _jsx("th", { style: th, children: "p95" })] }) }), _jsx("tbody", { children: payload.tools.map((row) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: row.tool }) }), _jsx("td", { style: td, children: row.invocations }), _jsxs("td", { style: td, children: [Math.round(row.failureRate * 100), "%"] }), _jsxs("td", { style: td, children: [row.p50Latency, "ms"] }), _jsxs("td", { style: td, children: [row.p95Latency, "ms"] })] }, row.tool))) })] })), payload.deadTools.length > 0 && (_jsxs(_Fragment, { children: [
|
|
95
|
+
return (_jsxs("div", { style: wrap, children: [header, _jsx(AlertBanner, { alerts: payload.alerts, t: t }), _jsx(Overview, { payload: payload, t: t }), payload.tools.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th }), _jsx("th", { style: th, children: t('tool') }), _jsx("th", { style: th, children: t('calls') }), _jsx("th", { style: th, children: t('failRate') }), _jsx("th", { style: th, children: "p50" }), _jsx("th", { style: th, children: "p95" })] }) }), _jsx("tbody", { children: payload.tools.map((row) => (_jsx(_Fragment, { children: _jsxs("tr", { style: clickRow, onClick: () => { }, children: [_jsx("td", { style: td, children: _jsx(ToolDetailRow, { tool: row.tool, t: t }) }), _jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: row.tool }) }), _jsx("td", { style: td, children: row.invocations }), _jsxs("td", { style: { ...td, color: row.failureRate > 0.3 ? '#e55' : undefined }, children: [Math.round(row.failureRate * 100), "%"] }), _jsxs("td", { style: td, children: [row.p50Latency, "ms"] }), _jsxs("td", { style: td, children: [row.p95Latency, "ms"] })] }, row.tool) }))) })] })), payload.deadTools.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDC80 ", t('deadTools')] }), _jsx("ul", { style: list, children: payload.deadTools.map((row) => _jsxs("li", { children: [_jsx("code", { style: { fontSize: 11 }, children: row.tool }), " \u2014 ", row.sessionsSinceLastUse, " ", t('sessionsUnused')] }, row.tool)) })] })), payload.failingTools.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\u26A0\uFE0F ", t('failingTools')] }), _jsx("ul", { style: list, children: payload.failingTools.map((row) => (_jsxs("li", { children: [_jsx("code", { style: { fontSize: 11 }, children: row.tool }), " \u00B7 ", Math.round(row.failureRate * 100), "%", row.lastError ? ` — ${row.lastError.slice(0, 60)}` : ''] }, row.tool))) })] })), payload.recommendations.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDCA1 ", t('recommendations')] }), _jsx("ul", { style: list, children: payload.recommendations.map((row, i) => _jsx("li", { children: row.message }, i)) })] }))] }));
|
|
58
96
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -11,5 +11,12 @@ export interface StatsService {
|
|
|
11
11
|
errorMessage?: string;
|
|
12
12
|
}): void;
|
|
13
13
|
summary(configuredTools?: string[]): ReturnType<ToolStats['summary']>;
|
|
14
|
+
query(range?: {
|
|
15
|
+
from?: number | undefined;
|
|
16
|
+
to?: number | undefined;
|
|
17
|
+
}, configuredTools?: string[]): ReturnType<ToolStats['summary']>;
|
|
18
|
+
detail(toolName: string): ReturnType<ToolStats['detail']>;
|
|
19
|
+
exportCSV(): string;
|
|
20
|
+
clearData(): void;
|
|
14
21
|
}
|
|
15
22
|
export declare function apply(ctx: Context): void;
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,10 @@ export function apply(ctx) {
|
|
|
8
8
|
const service = {
|
|
9
9
|
record: (call) => stats.record(call),
|
|
10
10
|
summary: (configuredTools) => stats.summary(configuredTools ?? []),
|
|
11
|
+
query: (range, configuredTools) => stats.query(range, configuredTools ?? []),
|
|
12
|
+
detail: (toolName) => stats.detail(toolName),
|
|
13
|
+
exportCSV: () => stats.exportCSV(),
|
|
14
|
+
clearData: () => stats.clearData(),
|
|
11
15
|
};
|
|
12
16
|
ctx.provide('toolStats', service);
|
|
13
17
|
registerStatsRoutes(ctx, service);
|
package/lib/routes.d.ts
CHANGED
|
@@ -2,4 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import type { StatsService } from './index.js';
|
|
3
3
|
import { STATS_PANEL_PATH } from './stats.js';
|
|
4
4
|
export { STATS_PANEL_PATH };
|
|
5
|
+
export declare const STATS_DETAIL_PATH = "/api/stats.detail";
|
|
6
|
+
export declare const STATS_EXPORT_PATH = "/api/stats.export";
|
|
7
|
+
export declare const STATS_CLEAR_PATH = "/api/stats.clear";
|
|
5
8
|
export declare function registerStatsRoutes(ctx: Context, stats: StatsService): void;
|
package/lib/routes.js
CHANGED
|
@@ -1,15 +1,63 @@
|
|
|
1
1
|
import { STATS_PANEL_PATH } from './stats.js';
|
|
2
2
|
export { STATS_PANEL_PATH };
|
|
3
|
+
export const STATS_DETAIL_PATH = '/api/stats.detail';
|
|
4
|
+
export const STATS_EXPORT_PATH = '/api/stats.export';
|
|
5
|
+
export const STATS_CLEAR_PATH = '/api/stats.clear';
|
|
3
6
|
export function registerStatsRoutes(ctx, stats) {
|
|
4
7
|
ctx.inject(['connection'], (connectionCtx) => {
|
|
5
8
|
const connection = connectionCtx.connection;
|
|
9
|
+
// Panel overview
|
|
6
10
|
connection.fetch.register({
|
|
7
11
|
path: STATS_PANEL_PATH,
|
|
8
12
|
methods: ['GET'],
|
|
9
13
|
requestBody: 'buffered',
|
|
10
|
-
fetch: () =>
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
fetch: (req) => {
|
|
15
|
+
const url = new URL(req.url, 'http://localhost');
|
|
16
|
+
const from = url.searchParams.get('from');
|
|
17
|
+
const to = url.searchParams.get('to');
|
|
18
|
+
const range = (from || to) ? { from: from ? Number(from) : undefined, to: to ? Number(to) : undefined } : undefined;
|
|
19
|
+
return Promise.resolve(Response.json(range ? stats.query(range) : stats.summary(), {
|
|
20
|
+
headers: { 'cache-control': 'no-store' },
|
|
21
|
+
}));
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
// Tool detail
|
|
25
|
+
connection.fetch.register({
|
|
26
|
+
path: STATS_DETAIL_PATH,
|
|
27
|
+
methods: ['GET'],
|
|
28
|
+
requestBody: 'buffered',
|
|
29
|
+
fetch: (req) => {
|
|
30
|
+
const url = new URL(req.url, 'http://localhost');
|
|
31
|
+
const tool = url.searchParams.get('tool');
|
|
32
|
+
if (!tool)
|
|
33
|
+
return Promise.resolve(Response.json({ error: 'tool required' }, { status: 400 }));
|
|
34
|
+
const detail = stats.detail(tool);
|
|
35
|
+
if (!detail)
|
|
36
|
+
return Promise.resolve(Response.json({ error: 'not found' }, { status: 404 }));
|
|
37
|
+
return Promise.resolve(Response.json(detail, { headers: { 'cache-control': 'no-store' } }));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
// Export CSV
|
|
41
|
+
connection.fetch.register({
|
|
42
|
+
path: STATS_EXPORT_PATH,
|
|
43
|
+
methods: ['GET'],
|
|
44
|
+
requestBody: 'buffered',
|
|
45
|
+
fetch: () => {
|
|
46
|
+
const csv = stats.exportCSV();
|
|
47
|
+
return Promise.resolve(new Response(csv, {
|
|
48
|
+
headers: { 'content-type': 'text/csv', 'content-disposition': 'attachment; filename=tool-stats.csv' },
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
// Clear data
|
|
53
|
+
connection.fetch.register({
|
|
54
|
+
path: STATS_CLEAR_PATH,
|
|
55
|
+
methods: ['POST'],
|
|
56
|
+
requestBody: 'buffered',
|
|
57
|
+
fetch: () => {
|
|
58
|
+
stats.clearData();
|
|
59
|
+
return Promise.resolve(Response.json({ ok: true }));
|
|
60
|
+
},
|
|
13
61
|
});
|
|
14
62
|
});
|
|
15
63
|
}
|
package/lib/stats.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolCall, PanelPayload } from './types.js';
|
|
1
|
+
import type { ToolCall, PanelPayload, ToolDetail, TimeRange } from './types.js';
|
|
2
2
|
export declare const STATS_PANEL_PATH = "/api/stats.panel";
|
|
3
3
|
export declare class ToolStats {
|
|
4
4
|
private projectDir;
|
|
@@ -6,4 +6,12 @@ export declare class ToolStats {
|
|
|
6
6
|
constructor(projectDir: string);
|
|
7
7
|
record(call: ToolCall): void;
|
|
8
8
|
summary(configuredTools?: string[]): PanelPayload;
|
|
9
|
+
/** Query calls with optional time range. */
|
|
10
|
+
query(range?: TimeRange, configuredTools?: string[]): PanelPayload;
|
|
11
|
+
/** Get detailed stats for one tool. */
|
|
12
|
+
detail(toolName: string): ToolDetail | null;
|
|
13
|
+
/** Export all calls as CSV. */
|
|
14
|
+
exportCSV(): string;
|
|
15
|
+
/** Clear all recorded data. */
|
|
16
|
+
clearData(): void;
|
|
9
17
|
}
|
package/lib/stats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StatsStore } from './store.js';
|
|
2
|
-
import { aggregate, findDeadTools, findFailingTools, recommend } from './analyzer.js';
|
|
2
|
+
import { aggregate, findDeadTools, findFailingTools, recommend, toolDetail, generateAlerts, overview } from './analyzer.js';
|
|
3
3
|
export const STATS_PANEL_PATH = '/api/stats.panel';
|
|
4
4
|
export class ToolStats {
|
|
5
5
|
projectDir;
|
|
@@ -20,6 +20,38 @@ export class ToolStats {
|
|
|
20
20
|
deadTools: findDeadTools(configuredTools, calls),
|
|
21
21
|
failingTools: findFailingTools(calls),
|
|
22
22
|
recommendations: recommend(configuredTools, calls),
|
|
23
|
+
overview: overview(calls),
|
|
24
|
+
alerts: generateAlerts(calls),
|
|
23
25
|
};
|
|
24
26
|
}
|
|
27
|
+
/** Query calls with optional time range. */
|
|
28
|
+
query(range, configuredTools = []) {
|
|
29
|
+
const calls = this.store.query(range?.from, range?.to);
|
|
30
|
+
const counters = aggregate(calls);
|
|
31
|
+
const tools = [...counters.values()].sort((a, b) => b.invocations - a.invocations);
|
|
32
|
+
return {
|
|
33
|
+
tools,
|
|
34
|
+
deadTools: findDeadTools(configuredTools, calls),
|
|
35
|
+
failingTools: findFailingTools(calls),
|
|
36
|
+
recommendations: recommend(configuredTools, calls),
|
|
37
|
+
overview: overview(calls),
|
|
38
|
+
alerts: generateAlerts(calls),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Get detailed stats for one tool. */
|
|
42
|
+
detail(toolName) {
|
|
43
|
+
const calls = this.store.readAll();
|
|
44
|
+
return toolDetail(calls, toolName);
|
|
45
|
+
}
|
|
46
|
+
/** Export all calls as CSV. */
|
|
47
|
+
exportCSV() {
|
|
48
|
+
const calls = this.store.readAll();
|
|
49
|
+
const header = 'tool,timestamp,success,latencyMs,sessionId,errorMessage';
|
|
50
|
+
const rows = calls.map((c) => [c.tool, c.timestamp, c.success, c.latencyMs, c.sessionId, c.errorMessage ?? ''].join(','));
|
|
51
|
+
return [header, ...rows].join('\n');
|
|
52
|
+
}
|
|
53
|
+
/** Clear all recorded data. */
|
|
54
|
+
clearData() {
|
|
55
|
+
this.store.clear();
|
|
56
|
+
}
|
|
25
57
|
}
|
package/lib/stats.web.js
CHANGED
|
@@ -42,7 +42,17 @@ var zh = {
|
|
|
42
42
|
"refresh": "\u5237\u65B0",
|
|
43
43
|
"loading": "\u6B63\u5728\u52A0\u8F7D\u2026",
|
|
44
44
|
"failed": "\u52A0\u8F7D\u5931\u8D25",
|
|
45
|
-
"retry": "\u91CD\u8BD5"
|
|
45
|
+
"retry": "\u91CD\u8BD5",
|
|
46
|
+
"totalCalls": "\u603B\u8C03\u7528",
|
|
47
|
+
"totalFailures": "\u603B\u5931\u8D25",
|
|
48
|
+
"sessions": "\u4F1A\u8BDD\u6570",
|
|
49
|
+
"avgLatency": "\u5E73\u5747\u5EF6\u8FDF",
|
|
50
|
+
"export": "\u5BFC\u51FA CSV",
|
|
51
|
+
"clear": "\u6E05\u7A7A",
|
|
52
|
+
"confirmClear": "\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u8BB0\u5F55\uFF1F",
|
|
53
|
+
"errorBreakdown": "\u9519\u8BEF\u5206\u7C7B",
|
|
54
|
+
"recentCalls": "\u6700\u8FD1\u8C03\u7528",
|
|
55
|
+
"sessionsUnused": "\u4F1A\u8BDD\u672A\u7528"
|
|
46
56
|
};
|
|
47
57
|
var en = {
|
|
48
58
|
"nav": "Tool Stats",
|
|
@@ -57,46 +67,158 @@ var en = {
|
|
|
57
67
|
"refresh": "Refresh",
|
|
58
68
|
"loading": "Loading\u2026",
|
|
59
69
|
"failed": "Failed to load",
|
|
60
|
-
"retry": "Retry"
|
|
70
|
+
"retry": "Retry",
|
|
71
|
+
"totalCalls": "Total Calls",
|
|
72
|
+
"totalFailures": "Total Failures",
|
|
73
|
+
"sessions": "Sessions",
|
|
74
|
+
"avgLatency": "Avg Latency",
|
|
75
|
+
"export": "Export CSV",
|
|
76
|
+
"clear": "Clear",
|
|
77
|
+
"confirmClear": "Clear all recorded data?",
|
|
78
|
+
"errorBreakdown": "Error Breakdown",
|
|
79
|
+
"recentCalls": "Recent Calls",
|
|
80
|
+
"sessionsUnused": "sessions unused"
|
|
61
81
|
};
|
|
62
82
|
|
|
63
83
|
// src/client/view.tsx
|
|
64
84
|
var import_react = require("react");
|
|
65
85
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
66
86
|
var PANEL_PATH = "/api/stats.panel";
|
|
67
|
-
var
|
|
87
|
+
var DETAIL_PATH = "/api/stats.detail";
|
|
88
|
+
var EXPORT_PATH = "/api/stats.export";
|
|
89
|
+
var CLEAR_PATH = "/api/stats.clear";
|
|
90
|
+
var wrap = { display: "flex", flexDirection: "column", gap: 12, maxWidth: 820, fontFamily: "inherit" };
|
|
68
91
|
var head = { display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap" };
|
|
69
92
|
var muted = { fontSize: 12, opacity: 0.75 };
|
|
70
93
|
var table = { borderCollapse: "collapse", width: "100%" };
|
|
71
94
|
var th = { textAlign: "left", padding: "4px 10px 4px 0", fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: "0.5px solid rgba(128,128,128,0.4)" };
|
|
72
95
|
var td = { padding: "6px 10px 6px 0", fontSize: 13, borderBottom: "0.5px solid rgba(128,128,128,0.18)" };
|
|
73
96
|
var list = { margin: 0, paddingLeft: 18, fontSize: 13 };
|
|
97
|
+
var card = { padding: "8px 12px", borderRadius: 6, border: "1px solid rgba(128,128,128,0.2)", fontSize: 13 };
|
|
98
|
+
var statBox = { ...card, flex: 1, minWidth: 100, textAlign: "center" };
|
|
99
|
+
var statRow = { display: "flex", gap: 12, flexWrap: "wrap" };
|
|
100
|
+
var btn = { fontSize: 12, cursor: "pointer", padding: "3px 10px", borderRadius: 4, border: "0.5px solid rgba(128,128,128,0.4)" };
|
|
101
|
+
var dangerBtn = { ...btn, color: "#e55", borderColor: "#e55" };
|
|
102
|
+
var alertCard = { ...card, display: "flex", alignItems: "center", gap: 8, marginTop: 4 };
|
|
103
|
+
var preBlock = { whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 11, maxHeight: 150, overflow: "auto", padding: 6, borderRadius: 4, background: "rgba(128,128,128,0.06)" };
|
|
104
|
+
var clickRow = { cursor: "pointer" };
|
|
74
105
|
function usePanel() {
|
|
75
106
|
const [state, setState] = (0, import_react.useState)({ payload: null, error: null });
|
|
76
107
|
const [tick, setTick] = (0, import_react.useState)(0);
|
|
77
|
-
const reload = (0, import_react.useCallback)(() => setTick((
|
|
108
|
+
const reload = (0, import_react.useCallback)(() => setTick((v) => v + 1), []);
|
|
78
109
|
(0, import_react.useEffect)(() => {
|
|
79
|
-
const
|
|
80
|
-
setState((
|
|
81
|
-
fetch(PANEL_PATH, { signal:
|
|
82
|
-
if (!
|
|
83
|
-
return
|
|
110
|
+
const c = new AbortController();
|
|
111
|
+
setState((p) => ({ ...p, error: null }));
|
|
112
|
+
fetch(PANEL_PATH, { signal: c.signal }).then(async (r) => {
|
|
113
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
114
|
+
return r.json();
|
|
84
115
|
}).then((payload) => {
|
|
85
|
-
if (!
|
|
86
|
-
}).catch((
|
|
87
|
-
if (
|
|
88
|
-
setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) });
|
|
116
|
+
if (!c.signal.aborted) setState({ payload, error: null });
|
|
117
|
+
}).catch((e) => {
|
|
118
|
+
if (!c.signal.aborted) setState({ payload: null, error: e instanceof Error ? e.message : String(e) });
|
|
89
119
|
});
|
|
90
|
-
return () =>
|
|
120
|
+
return () => c.abort();
|
|
91
121
|
}, [tick]);
|
|
92
122
|
return { ...state, reload };
|
|
93
123
|
}
|
|
124
|
+
function AlertBanner({ alerts, t }) {
|
|
125
|
+
if (alerts.length === 0) return null;
|
|
126
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: alerts.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...alertCard, borderColor: a.level === "error" ? "#e55" : "#ed0", background: a.level === "error" ? "rgba(255,80,80,0.06)" : "rgba(255,200,0,0.06)" }, children: [
|
|
127
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 16 }, children: a.level === "error" ? "\u{1F534}" : "\u{1F7E1}" }),
|
|
128
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
129
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: a.tool }),
|
|
130
|
+
": ",
|
|
131
|
+
a.message
|
|
132
|
+
] })
|
|
133
|
+
] }, i)) });
|
|
134
|
+
}
|
|
135
|
+
function Overview({ payload, t }) {
|
|
136
|
+
const o = payload.overview;
|
|
137
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statRow, children: [
|
|
138
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 18, fontWeight: 700 }, children: o.totalCalls }),
|
|
140
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("totalCalls") })
|
|
141
|
+
] }),
|
|
142
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 18, fontWeight: 700, color: o.totalFailures > 0 ? "#e55" : void 0 }, children: o.totalFailures }),
|
|
144
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("totalFailures") })
|
|
145
|
+
] }),
|
|
146
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
147
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 18, fontWeight: 700 }, children: [
|
|
148
|
+
Math.round(o.overallFailureRate * 100),
|
|
149
|
+
"%"
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("failRate") })
|
|
152
|
+
] }),
|
|
153
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
154
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 18, fontWeight: 700 }, children: o.activeSessions }),
|
|
155
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("sessions") })
|
|
156
|
+
] }),
|
|
157
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
158
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 18, fontWeight: 700 }, children: [
|
|
159
|
+
o.avgLatencyMs,
|
|
160
|
+
"ms"
|
|
161
|
+
] }),
|
|
162
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("avgLatency") })
|
|
163
|
+
] })
|
|
164
|
+
] });
|
|
165
|
+
}
|
|
166
|
+
function ToolDetailRow({ tool, t }) {
|
|
167
|
+
const [detail, setDetail] = (0, import_react.useState)(null);
|
|
168
|
+
const [loading, setLoading] = (0, import_react.useState)(false);
|
|
169
|
+
const toggle = (0, import_react.useCallback)(async () => {
|
|
170
|
+
if (detail) {
|
|
171
|
+
setDetail(null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
setLoading(true);
|
|
175
|
+
try {
|
|
176
|
+
const r = await fetch(`${DETAIL_PATH}?tool=${encodeURIComponent(tool)}`);
|
|
177
|
+
if (r.ok) setDetail(await r.json());
|
|
178
|
+
} finally {
|
|
179
|
+
setLoading(false);
|
|
180
|
+
}
|
|
181
|
+
}, [detail, tool]);
|
|
182
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
183
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { style: clickRow, onClick: toggle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: loading ? "\u2026" : detail ? "\u25BC" : "\u25B6" }) }),
|
|
184
|
+
detail && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { colSpan: 6, style: { padding: "4px 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: card, children: [
|
|
185
|
+
detail.errorBreakdown.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 8 }, children: [
|
|
186
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 12 }, children: t("errorBreakdown") }),
|
|
187
|
+
detail.errorBreakdown.map((e, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...preBlock, marginTop: 4 }, children: [
|
|
188
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { color: "#e55", fontWeight: 600 }, children: [
|
|
189
|
+
"\xD7",
|
|
190
|
+
e.count
|
|
191
|
+
] }),
|
|
192
|
+
" ",
|
|
193
|
+
e.message
|
|
194
|
+
] }, i))
|
|
195
|
+
] }),
|
|
196
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 12 }, children: t("recentCalls") }),
|
|
197
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("table", { style: { ...table, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: detail.recentCalls.slice(0, 10).map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
|
|
198
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: c.success ? "\u2713" : "\u2717" }),
|
|
199
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
|
|
200
|
+
c.latencyMs,
|
|
201
|
+
"ms"
|
|
202
|
+
] }),
|
|
203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: new Date(c.timestamp).toLocaleTimeString() }),
|
|
204
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: { ...td, fontSize: 10, opacity: 0.7 }, children: c.errorMessage?.slice(0, 60) })
|
|
205
|
+
] }, i)) }) })
|
|
206
|
+
] }) }) })
|
|
207
|
+
] });
|
|
208
|
+
}
|
|
94
209
|
function ToolStatsPanel({ t }) {
|
|
95
210
|
const { payload, error, reload } = usePanel();
|
|
211
|
+
const handleClear = (0, import_react.useCallback)(() => {
|
|
212
|
+
if (!confirm(t("confirmClear"))) return;
|
|
213
|
+
fetch(CLEAR_PATH, { method: "POST" }).then(() => reload()).catch(() => {
|
|
214
|
+
});
|
|
215
|
+
}, [t, reload]);
|
|
96
216
|
const header = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { style: head, children: [
|
|
97
217
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 13 }, children: t("title") }),
|
|
98
218
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
|
|
99
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
219
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: EXPORT_PATH, download: true, style: { ...btn, textDecoration: "none", color: "inherit" }, children: t("export") }),
|
|
220
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: dangerBtn, onClick: handleClear, children: t("clear") }),
|
|
221
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: btn, onClick: reload, children: t("refresh") })
|
|
100
222
|
] });
|
|
101
223
|
if (error !== null) {
|
|
102
224
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
|
|
@@ -106,24 +228,29 @@ function ToolStatsPanel({ t }) {
|
|
|
106
228
|
": ",
|
|
107
229
|
error
|
|
108
230
|
] }),
|
|
109
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, style: { alignSelf: "flex-start"
|
|
231
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, style: { ...btn, alignSelf: "flex-start" }, children: t("retry") })
|
|
110
232
|
] });
|
|
111
233
|
}
|
|
112
234
|
if (payload === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, "aria-live": "polite", children: t("loading") });
|
|
113
235
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
|
|
114
236
|
header,
|
|
237
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertBanner, { alerts: payload.alerts, t }),
|
|
238
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Overview, { payload, t }),
|
|
115
239
|
payload.tools.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t("empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: table, children: [
|
|
116
240
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
|
|
241
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th }),
|
|
117
242
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("tool") }),
|
|
118
243
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("calls") }),
|
|
119
244
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("failRate") }),
|
|
120
245
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: "p50" }),
|
|
121
246
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: "p95" })
|
|
122
247
|
] }) }),
|
|
123
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: payload.tools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
|
|
248
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: payload.tools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { style: clickRow, onClick: () => {
|
|
249
|
+
}, children: [
|
|
250
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolDetailRow, { tool: row.tool, t }) }),
|
|
124
251
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }) }),
|
|
125
252
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: row.invocations }),
|
|
126
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
|
|
253
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: { ...td, color: row.failureRate > 0.3 ? "#e55" : void 0 }, children: [
|
|
127
254
|
Math.round(row.failureRate * 100),
|
|
128
255
|
"%"
|
|
129
256
|
] }),
|
|
@@ -135,24 +262,40 @@ function ToolStatsPanel({ t }) {
|
|
|
135
262
|
row.p95Latency,
|
|
136
263
|
"ms"
|
|
137
264
|
] })
|
|
138
|
-
] }, row.tool)) })
|
|
265
|
+
] }, row.tool) })) })
|
|
139
266
|
] }),
|
|
140
267
|
payload.deadTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
141
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
142
|
-
|
|
268
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
269
|
+
"\u{1F480} ",
|
|
270
|
+
t("deadTools")
|
|
271
|
+
] }),
|
|
272
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: list, children: payload.deadTools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
|
|
273
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }),
|
|
274
|
+
" \u2014 ",
|
|
275
|
+
row.sessionsSinceLastUse,
|
|
276
|
+
" ",
|
|
277
|
+
t("sessionsUnused")
|
|
278
|
+
] }, row.tool)) })
|
|
143
279
|
] }),
|
|
144
280
|
payload.failingTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
145
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
281
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
282
|
+
"\u26A0\uFE0F ",
|
|
283
|
+
t("failingTools")
|
|
284
|
+
] }),
|
|
146
285
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: list, children: payload.failingTools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
|
|
147
286
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }),
|
|
148
287
|
" \xB7 ",
|
|
149
288
|
Math.round(row.failureRate * 100),
|
|
150
|
-
"%"
|
|
289
|
+
"%",
|
|
290
|
+
row.lastError ? ` \u2014 ${row.lastError.slice(0, 60)}` : ""
|
|
151
291
|
] }, row.tool)) })
|
|
152
292
|
] }),
|
|
153
293
|
payload.recommendations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
154
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
155
|
-
|
|
294
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
295
|
+
"\u{1F4A1} ",
|
|
296
|
+
t("recommendations")
|
|
297
|
+
] }),
|
|
298
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: list, children: payload.recommendations.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: row.message }, i)) })
|
|
156
299
|
] })
|
|
157
300
|
] });
|
|
158
301
|
}
|
package/lib/stats.web.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client/index.tsx", "../src/client/locales.ts", "../src/client/view.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * Browser half of dsh-tool-stats: its page inside Settings.\n *\n * Two services are injected by their runtime identity, not by the packages that provide\n * them: the settings shell is `slots`, the dictionary registry is `locale`. The label and\n * every string inside the component are translated through this plugin's own namespace,\n * which is why the page follows the UI language instead of the plugin author's.\n *\n * @module client\n */\n\nimport type { ComponentType } from 'react'\n\nimport { NS, en, zh } from './locales.js'\nimport { ToolStatsPanel } from './view.js'\n\nexport const inject = ['slots', 'locale']\n\ninterface SlotsService {\n inject(name: string, register: () => void): void\n register(\n options: { name: string; id: string; order: number; label: () => string; locale: string },\n component: ComponentType<{ t: (key: string, params?: Record<string, unknown>) => string }>,\n ): unknown\n}\n\ninterface ClientContext {\n slots: SlotsService\n locale: {\n register(ns: string, dicts: { zh: unknown; en: unknown }): () => void\n bind(ns: string): (key: string, params?: Record<string, unknown>) => string\n }\n effect(callback: () => unknown, label?: string): unknown\n}\n\nexport function apply(ctx: ClientContext): void {\n // `zh` is the key-set source of truth, matching the official client plugins.\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-tool-stats: dictionaries')\n\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n {\n name: 'settings.section',\n id: 'tool-stats',\n order: 43,\n label: () => ctx.locale.bind(NS)('nav'),\n locale: NS,\n },\n ToolStatsPanel,\n ))\n}\n", "/**\n * Dictionaries for the Tool Stats page.\n *\n * `zh` is the key-set source of truth, as in the official client plugins, and `en` is\n * typed against it: a key translated in one language but not the other fails the build\n * instead of silently rendering the raw key.\n *\n * @module client/locales\n */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'toolStats'\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'nav': '\u5DE5\u5177\u7EDF\u8BA1',\n 'title': '\u5DE5\u5177\u7EDF\u8BA1',\n 'tool': '\u5DE5\u5177',\n 'calls': '\u8C03\u7528\u6B21\u6570',\n 'failRate': '\u5931\u8D25\u7387',\n 'deadTools': '\u4ECE\u672A\u8C03\u7528\u7684\u5DE5\u5177',\n 'failingTools': '\u9AD8\u5931\u8D25\u7387\u5DE5\u5177',\n 'recommendations': '\u5EFA\u8BAE',\n 'empty': '\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u5DE5\u5177\u8C03\u7528\u3002',\n 'refresh': '\u5237\u65B0',\n 'loading': '\u6B63\u5728\u52A0\u8F7D\u2026',\n 'failed': '\u52A0\u8F7D\u5931\u8D25',\n 'retry': '\u91CD\u8BD5',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: typeof zh = {\n 'nav': 'Tool Stats',\n 'title': 'Tool Stats',\n 'tool': 'Tool',\n 'calls': 'Calls',\n 'failRate': 'Failure',\n 'deadTools': 'Dead tools',\n 'failingTools': 'Failing tools',\n 'recommendations': 'Recommendations',\n 'empty': 'No tool calls recorded yet.',\n 'refresh': 'Refresh',\n 'loading': 'Loading\u2026',\n 'failed': 'Failed to load',\n 'retry': 'Retry',\n}\n", "/**\n * Pure rendering half of the toolStats page.\n *\n * Separate from `index.tsx` so a static render can assert in Node what the page draws \u2014\n * the shipped bundle is a loader factory only a browser can run. Every user-visible\n * string comes from the `t` seat the renderer binds from this plugin's namespace, so the\n * page follows the UI language; no copy is hardcoded here.\n *\n * @module client/view\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { CSSProperties } from 'react'\n\nimport type { PanelPayload } from '../types.js'\n\n/** The translate seat the renderer binds from this plugin's locale namespace. */\nexport type Translate = (key: string, params?: Record<string, unknown>) => string\n\nexport interface PanelProps {\n /** Bound translate function for this plugin's namespace. */\n t: Translate\n}\n\n/** Panel route registered by the host half on the web connection. */\nconst PANEL_PATH = \"/api/stats.panel\"\n\nconst wrap: CSSProperties = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 760, fontFamily: 'inherit' }\nconst head: CSSProperties = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }\nconst muted: CSSProperties = { fontSize: 12, opacity: 0.75 }\nconst table: CSSProperties = { borderCollapse: 'collapse', width: '100%' }\nconst th: CSSProperties = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' }\nconst td: CSSProperties = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' }\nconst list: CSSProperties = { margin: 0, paddingLeft: 18, fontSize: 13 }\n\ninterface PanelState {\n payload: PanelPayload | null\n error: string | null\n}\n\n/** Fetch the host panel payload; `reload` re-runs the request. */\nexport function usePanel(): PanelState & { reload: () => void } {\n const [state, setState] = useState<PanelState>({ payload: null, error: null })\n const [tick, setTick] = useState(0)\n const reload = useCallback(() => setTick((value) => value + 1), [])\n\n useEffect(() => {\n const controller = new AbortController()\n setState((previous) => ({ ...previous, error: null }))\n fetch(PANEL_PATH, { signal: controller.signal })\n .then(async (response) => {\n if (!response.ok) throw new Error(String(response.status))\n return response.json() as Promise<PanelPayload>\n })\n .then((payload) => {\n if (!controller.signal.aborted) setState({ payload, error: null })\n })\n .catch((cause: unknown) => {\n if (controller.signal.aborted) return\n setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) })\n })\n return () => controller.abort()\n }, [tick])\n\n return { ...state, reload }\n}\n\nexport function ToolStatsPanel({ t }: PanelProps) {\n const { payload, error, reload } = usePanel()\n const header = (\n <header style={head}>\n <strong style={{ fontSize: 13 }}>{t('title')}</strong>\n <span style={{ flex: 1 }} />\n <button type=\"button\" onClick={reload} style={{ fontSize: 12 }}>{t('refresh')}</button>\n </header>\n )\n if (error !== null) {\n return (\n <div style={wrap}>\n {header}\n <p role=\"alert\" style={{ margin: 0, fontSize: 13 }}>{t('failed')}: {error}</p>\n <button type=\"button\" onClick={reload} style={{ alignSelf: 'flex-start', fontSize: 12 }}>{t('retry')}</button>\n </div>\n )\n }\n if (payload === null) return <p style={muted} aria-live=\"polite\">{t('loading')}</p>\n return (\n <div style={wrap}>\n {header}\n {payload.tools.length === 0 ? (\n <p style={{ margin: 0, fontSize: 13, opacity: 0.8 }}>{t('empty')}</p>\n ) : (\n <table style={table}>\n <thead>\n <tr>\n <th style={th}>{t('tool')}</th><th style={th}>{t('calls')}</th>\n <th style={th}>{t('failRate')}</th><th style={th}>p50</th><th style={th}>p95</th>\n </tr>\n </thead>\n <tbody>\n {payload.tools.map((row) => (\n <tr key={row.tool}>\n <td style={td}><code style={{ fontSize: 11 }}>{row.tool}</code></td>\n <td style={td}>{row.invocations}</td>\n <td style={td}>{Math.round(row.failureRate * 100)}%</td>\n <td style={td}>{row.p50Latency}ms</td>\n <td style={td}>{row.p95Latency}ms</td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n {payload.deadTools.length > 0 && (\n <>\n <strong style={{ fontSize: 13 }}>{t('deadTools')}</strong>\n <ul style={list}>\n {payload.deadTools.map((row) => <li key={row.tool}><code style={{ fontSize: 11 }}>{row.tool}</code></li>)}\n </ul>\n </>\n )}\n {payload.failingTools.length > 0 && (\n <>\n <strong style={{ fontSize: 13 }}>{t('failingTools')}</strong>\n <ul style={list}>\n {payload.failingTools.map((row) => (\n <li key={row.tool}><code style={{ fontSize: 11 }}>{row.tool}</code> \u00B7 {Math.round(row.failureRate * 100)}%</li>\n ))}\n </ul>\n </>\n )}\n {payload.recommendations.length > 0 && (\n <>\n <strong style={{ fontSize: 13 }}>{t('recommendations')}</strong>\n <ul style={list}>\n {payload.recommendations.map((row, index) => <li key={index}>{row.message}</li>)}\n </ul>\n </>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWO,IAAM,KAAK;AAGX,IAAM,KAAK;AAAA,EAChB,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,mBAAmB;AAAA,EACnB,SAAS;AAAA,EACT,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;
|
|
4
|
+
"sourcesContent": ["/**\n * Browser half of dsh-tool-stats: its page inside Settings.\n *\n * Two services are injected by their runtime identity, not by the packages that provide\n * them: the settings shell is `slots`, the dictionary registry is `locale`. The label and\n * every string inside the component are translated through this plugin's own namespace,\n * which is why the page follows the UI language instead of the plugin author's.\n *\n * @module client\n */\n\nimport type { ComponentType } from 'react'\n\nimport { NS, en, zh } from './locales.js'\nimport { ToolStatsPanel } from './view.js'\n\nexport const inject = ['slots', 'locale']\n\ninterface SlotsService {\n inject(name: string, register: () => void): void\n register(\n options: { name: string; id: string; order: number; label: () => string; locale: string },\n component: ComponentType<{ t: (key: string, params?: Record<string, unknown>) => string }>,\n ): unknown\n}\n\ninterface ClientContext {\n slots: SlotsService\n locale: {\n register(ns: string, dicts: { zh: unknown; en: unknown }): () => void\n bind(ns: string): (key: string, params?: Record<string, unknown>) => string\n }\n effect(callback: () => unknown, label?: string): unknown\n}\n\nexport function apply(ctx: ClientContext): void {\n // `zh` is the key-set source of truth, matching the official client plugins.\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-tool-stats: dictionaries')\n\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n {\n name: 'settings.section',\n id: 'tool-stats',\n order: 43,\n label: () => ctx.locale.bind(NS)('nav'),\n locale: NS,\n },\n ToolStatsPanel,\n ))\n}\n", "/**\n * Dictionaries for the Tool Stats page.\n *\n * `zh` is the key-set source of truth, as in the official client plugins, and `en` is\n * typed against it: a key translated in one language but not the other fails the build\n * instead of silently rendering the raw key.\n *\n * @module client/locales\n */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'toolStats'\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'nav': '\u5DE5\u5177\u7EDF\u8BA1',\n 'title': '\u5DE5\u5177\u7EDF\u8BA1',\n 'tool': '\u5DE5\u5177',\n 'calls': '\u8C03\u7528\u6B21\u6570',\n 'failRate': '\u5931\u8D25\u7387',\n 'deadTools': '\u4ECE\u672A\u8C03\u7528\u7684\u5DE5\u5177',\n 'failingTools': '\u9AD8\u5931\u8D25\u7387\u5DE5\u5177',\n 'recommendations': '\u5EFA\u8BAE',\n 'empty': '\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u5DE5\u5177\u8C03\u7528\u3002',\n 'refresh': '\u5237\u65B0',\n 'loading': '\u6B63\u5728\u52A0\u8F7D\u2026',\n 'failed': '\u52A0\u8F7D\u5931\u8D25',\n 'retry': '\u91CD\u8BD5',\n 'totalCalls': '\u603B\u8C03\u7528',\n 'totalFailures': '\u603B\u5931\u8D25',\n 'sessions': '\u4F1A\u8BDD\u6570',\n 'avgLatency': '\u5E73\u5747\u5EF6\u8FDF',\n 'export': '\u5BFC\u51FA CSV',\n 'clear': '\u6E05\u7A7A',\n 'confirmClear': '\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u8BB0\u5F55\uFF1F',\n 'errorBreakdown': '\u9519\u8BEF\u5206\u7C7B',\n 'recentCalls': '\u6700\u8FD1\u8C03\u7528',\n 'sessionsUnused': '\u4F1A\u8BDD\u672A\u7528',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: typeof zh = {\n 'nav': 'Tool Stats',\n 'title': 'Tool Stats',\n 'tool': 'Tool',\n 'calls': 'Calls',\n 'failRate': 'Failure',\n 'deadTools': 'Dead tools',\n 'failingTools': 'Failing tools',\n 'recommendations': 'Recommendations',\n 'empty': 'No tool calls recorded yet.',\n 'refresh': 'Refresh',\n 'loading': 'Loading\u2026',\n 'failed': 'Failed to load',\n 'retry': 'Retry',\n 'totalCalls': 'Total Calls',\n 'totalFailures': 'Total Failures',\n 'sessions': 'Sessions',\n 'avgLatency': 'Avg Latency',\n 'export': 'Export CSV',\n 'clear': 'Clear',\n 'confirmClear': 'Clear all recorded data?',\n 'errorBreakdown': 'Error Breakdown',\n 'recentCalls': 'Recent Calls',\n 'sessionsUnused': 'sessions unused',\n}\n", "/**\n * Pure rendering half of the toolStats page.\n *\n * Separate from `index.tsx` so a static render can assert in Node what the page draws \u2014\n * the shipped bundle is a loader factory only a browser can run. Every user-visible\n * string comes from the `t` seat the renderer binds from this plugin's namespace, so the\n * page follows the UI language; no copy is hardcoded here.\n *\n * @module client/view\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { CSSProperties } from 'react'\n\nimport type { PanelPayload, ToolDetail, Alert } from '../types.js'\n\nexport type Translate = (key: string, params?: Record<string, unknown>) => string\n\nexport interface PanelProps {\n t: Translate\n}\n\nconst PANEL_PATH = '/api/stats.panel'\nconst DETAIL_PATH = '/api/stats.detail'\nconst EXPORT_PATH = '/api/stats.export'\nconst CLEAR_PATH = '/api/stats.clear'\n\nconst wrap: CSSProperties = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 820, fontFamily: 'inherit' }\nconst head: CSSProperties = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }\nconst muted: CSSProperties = { fontSize: 12, opacity: 0.75 }\nconst table: CSSProperties = { borderCollapse: 'collapse', width: '100%' }\nconst th: CSSProperties = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' }\nconst td: CSSProperties = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' }\nconst list: CSSProperties = { margin: 0, paddingLeft: 18, fontSize: 13 }\nconst card: CSSProperties = { padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.2)', fontSize: 13 }\nconst statBox: CSSProperties = { ...card, flex: 1, minWidth: 100, textAlign: 'center' as const }\nconst statRow: CSSProperties = { display: 'flex', gap: 12, flexWrap: 'wrap' }\nconst btn: CSSProperties = { fontSize: 12, cursor: 'pointer', padding: '3px 10px', borderRadius: 4, border: '0.5px solid rgba(128,128,128,0.4)' }\nconst dangerBtn: CSSProperties = { ...btn, color: '#e55', borderColor: '#e55' }\nconst alertCard: CSSProperties = { ...card, display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }\nconst preBlock: CSSProperties = { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 11, maxHeight: 150, overflow: 'auto', padding: 6, borderRadius: 4, background: 'rgba(128,128,128,0.06)' }\nconst clickRow: CSSProperties = { cursor: 'pointer' }\n\ninterface PanelState {\n payload: PanelPayload | null\n error: string | null\n}\n\nexport function usePanel(): PanelState & { reload: () => void } {\n const [state, setState] = useState<PanelState>({ payload: null, error: null })\n const [tick, setTick] = useState(0)\n const reload = useCallback(() => setTick((v) => v + 1), [])\n useEffect(() => {\n const c = new AbortController()\n setState((p) => ({ ...p, error: null }))\n fetch(PANEL_PATH, { signal: c.signal })\n .then(async (r) => { if (!r.ok) throw new Error(String(r.status)); return r.json() as Promise<PanelPayload> })\n .then((payload) => { if (!c.signal.aborted) setState({ payload, error: null }) })\n .catch((e: unknown) => { if (!c.signal.aborted) setState({ payload: null, error: e instanceof Error ? e.message : String(e) }) })\n return () => c.abort()\n }, [tick])\n return { ...state, reload }\n}\n\n// --- Alert banner ---\nfunction AlertBanner({ alerts, t }: { alerts: Alert[]; t: Translate }) {\n if (alerts.length === 0) return null\n return (\n <div>\n {alerts.map((a, i) => (\n <div key={i} style={{ ...alertCard, borderColor: a.level === 'error' ? '#e55' : '#ed0', background: a.level === 'error' ? 'rgba(255,80,80,0.06)' : 'rgba(255,200,0,0.06)' }}>\n <span style={{ fontSize: 16 }}>{a.level === 'error' ? '\uD83D\uDD34' : '\uD83D\uDFE1'}</span>\n <span><strong>{a.tool}</strong>: {a.message}</span>\n </div>\n ))}\n </div>\n )\n}\n\n// --- Overview stats ---\nfunction Overview({ payload, t }: { payload: PanelPayload; t: Translate }) {\n const o = payload.overview\n return (\n <div style={statRow}>\n <div style={statBox}><div style={{ fontSize: 18, fontWeight: 700 }}>{o.totalCalls}</div><div style={muted}>{t('totalCalls')}</div></div>\n <div style={statBox}><div style={{ fontSize: 18, fontWeight: 700, color: o.totalFailures > 0 ? '#e55' : undefined }}>{o.totalFailures}</div><div style={muted}>{t('totalFailures')}</div></div>\n <div style={statBox}><div style={{ fontSize: 18, fontWeight: 700 }}>{Math.round(o.overallFailureRate * 100)}%</div><div style={muted}>{t('failRate')}</div></div>\n <div style={statBox}><div style={{ fontSize: 18, fontWeight: 700 }}>{o.activeSessions}</div><div style={muted}>{t('sessions')}</div></div>\n <div style={statBox}><div style={{ fontSize: 18, fontWeight: 700 }}>{o.avgLatencyMs}ms</div><div style={muted}>{t('avgLatency')}</div></div>\n </div>\n )\n}\n\n// --- Tool detail expansion ---\nfunction ToolDetailRow({ tool, t }: { tool: string; t: Translate }) {\n const [detail, setDetail] = useState<ToolDetail | null>(null)\n const [loading, setLoading] = useState(false)\n\n const toggle = useCallback(async () => {\n if (detail) { setDetail(null); return }\n setLoading(true)\n try {\n const r = await fetch(`${DETAIL_PATH}?tool=${encodeURIComponent(tool)}`)\n if (r.ok) setDetail(await r.json() as ToolDetail)\n } finally { setLoading(false) }\n }, [detail, tool])\n\n return (\n <>\n <tr style={clickRow} onClick={toggle}>\n <td style={td}>{loading ? '\u2026' : detail ? '\u25BC' : '\u25B6'}</td>\n </tr>\n {detail && (\n <tr>\n <td colSpan={6} style={{ padding: '4px 0' }}>\n <div style={card}>\n {/* Error breakdown */}\n {detail.errorBreakdown.length > 0 && (\n <div style={{ marginBottom: 8 }}>\n <strong style={{ fontSize: 12 }}>{t('errorBreakdown')}</strong>\n {detail.errorBreakdown.map((e, i) => (\n <div key={i} style={{ ...preBlock, marginTop: 4 }}>\n <span style={{ color: '#e55', fontWeight: 600 }}>\u00D7{e.count}</span> {e.message}\n </div>\n ))}\n </div>\n )}\n {/* Recent calls */}\n <strong style={{ fontSize: 12 }}>{t('recentCalls')}</strong>\n <table style={{ ...table, marginTop: 4 }}>\n <tbody>\n {detail.recentCalls.slice(0, 10).map((c, i) => (\n <tr key={i}>\n <td style={td}>{c.success ? '\u2713' : '\u2717'}</td>\n <td style={td}>{c.latencyMs}ms</td>\n <td style={td}>{new Date(c.timestamp).toLocaleTimeString()}</td>\n <td style={{ ...td, fontSize: 10, opacity: 0.7 }}>{c.errorMessage?.slice(0, 60)}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </td>\n </tr>\n )}\n </>\n )\n}\n\nexport function ToolStatsPanel({ t }: PanelProps) {\n const { payload, error, reload } = usePanel()\n\n const handleClear = useCallback(() => {\n if (!confirm(t('confirmClear'))) return\n fetch(CLEAR_PATH, { method: 'POST' }).then(() => reload()).catch(() => {})\n }, [t, reload])\n\n const header = (\n <header style={head}>\n <strong style={{ fontSize: 13 }}>{t('title')}</strong>\n <span style={{ flex: 1 }} />\n <a href={EXPORT_PATH} download style={{ ...btn, textDecoration: 'none', color: 'inherit' }}>{t('export')}</a>\n <button type=\"button\" style={dangerBtn} onClick={handleClear}>{t('clear')}</button>\n <button type=\"button\" style={btn} onClick={reload}>{t('refresh')}</button>\n </header>\n )\n\n if (error !== null) {\n return (\n <div style={wrap}>\n {header}\n <p role=\"alert\" style={{ margin: 0, fontSize: 13 }}>{t('failed')}: {error}</p>\n <button type=\"button\" onClick={reload} style={{ ...btn, alignSelf: 'flex-start' }}>{t('retry')}</button>\n </div>\n )\n }\n if (payload === null) return <p style={muted} aria-live=\"polite\">{t('loading')}</p>\n\n return (\n <div style={wrap}>\n {header}\n\n {/* Alerts */}\n <AlertBanner alerts={payload.alerts} t={t} />\n\n {/* Overview */}\n <Overview payload={payload} t={t} />\n\n {/* Tool table */}\n {payload.tools.length === 0 ? (\n <p style={{ margin: 0, fontSize: 13, opacity: 0.8 }}>{t('empty')}</p>\n ) : (\n <table style={table}>\n <thead>\n <tr>\n <th style={th} /><th style={th}>{t('tool')}</th><th style={th}>{t('calls')}</th>\n <th style={th}>{t('failRate')}</th><th style={th}>p50</th><th style={th}>p95</th>\n </tr>\n </thead>\n <tbody>\n {payload.tools.map((row) => (\n <>\n <tr key={row.tool} style={clickRow} onClick={() => {}}>\n <td style={td}><ToolDetailRow tool={row.tool} t={t} /></td>\n <td style={td}><code style={{ fontSize: 11 }}>{row.tool}</code></td>\n <td style={td}>{row.invocations}</td>\n <td style={{ ...td, color: row.failureRate > 0.3 ? '#e55' : undefined }}>{Math.round(row.failureRate * 100)}%</td>\n <td style={td}>{row.p50Latency}ms</td>\n <td style={td}>{row.p95Latency}ms</td>\n </tr>\n </>\n ))}\n </tbody>\n </table>\n )}\n\n {/* Dead tools */}\n {payload.deadTools.length > 0 && (\n <>\n <strong style={{ fontSize: 13 }}>\uD83D\uDC80 {t('deadTools')}</strong>\n <ul style={list}>\n {payload.deadTools.map((row) => <li key={row.tool}><code style={{ fontSize: 11 }}>{row.tool}</code> \u2014 {row.sessionsSinceLastUse} {t('sessionsUnused')}</li>)}\n </ul>\n </>\n )}\n\n {/* Failing tools */}\n {payload.failingTools.length > 0 && (\n <>\n <strong style={{ fontSize: 13 }}>\u26A0\uFE0F {t('failingTools')}</strong>\n <ul style={list}>\n {payload.failingTools.map((row) => (\n <li key={row.tool}><code style={{ fontSize: 11 }}>{row.tool}</code> \u00B7 {Math.round(row.failureRate * 100)}%{row.lastError ? ` \u2014 ${row.lastError.slice(0, 60)}` : ''}</li>\n ))}\n </ul>\n </>\n )}\n\n {/* Recommendations */}\n {payload.recommendations.length > 0 && (\n <>\n <strong style={{ fontSize: 13 }}>\uD83D\uDCA1 {t('recommendations')}</strong>\n <ul style={list}>\n {payload.recommendations.map((row, i) => <li key={i}>{row.message}</li>)}\n </ul>\n </>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/lib/store.d.ts
CHANGED
package/lib/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
export class StatsStore {
|
|
4
4
|
projectDir;
|
|
@@ -26,4 +26,18 @@ export class StatsStore {
|
|
|
26
26
|
.filter(Boolean)
|
|
27
27
|
.map((line) => JSON.parse(line));
|
|
28
28
|
}
|
|
29
|
+
query(from, to) {
|
|
30
|
+
const all = this.readAll();
|
|
31
|
+
return all.filter((c) => {
|
|
32
|
+
if (from && c.timestamp < from)
|
|
33
|
+
return false;
|
|
34
|
+
if (to && c.timestamp > to)
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
clear() {
|
|
40
|
+
if (existsSync(this.callsPath))
|
|
41
|
+
writeFileSync(this.callsPath, '', 'utf8');
|
|
42
|
+
}
|
|
29
43
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -32,9 +32,43 @@ export interface Recommendation {
|
|
|
32
32
|
tools: string[];
|
|
33
33
|
estimatedTokenSavings?: number;
|
|
34
34
|
}
|
|
35
|
+
export interface TimeRange {
|
|
36
|
+
from?: number | undefined;
|
|
37
|
+
to?: number | undefined;
|
|
38
|
+
}
|
|
39
|
+
export interface DailyToolTrend {
|
|
40
|
+
date: string;
|
|
41
|
+
invocations: number;
|
|
42
|
+
failures: number;
|
|
43
|
+
avgLatencyMs: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ToolDetail {
|
|
46
|
+
counter: ToolCounter;
|
|
47
|
+
recentCalls: ToolCall[];
|
|
48
|
+
trend: DailyToolTrend[];
|
|
49
|
+
errorBreakdown: {
|
|
50
|
+
message: string;
|
|
51
|
+
count: number;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
export interface Alert {
|
|
55
|
+
level: 'warn' | 'error';
|
|
56
|
+
tool: string;
|
|
57
|
+
message: string;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
}
|
|
60
|
+
export interface ToolStatsOverview {
|
|
61
|
+
totalCalls: number;
|
|
62
|
+
totalFailures: number;
|
|
63
|
+
overallFailureRate: number;
|
|
64
|
+
activeSessions: number;
|
|
65
|
+
avgLatencyMs: number;
|
|
66
|
+
}
|
|
35
67
|
export interface PanelPayload {
|
|
36
68
|
tools: ToolCounter[];
|
|
37
69
|
deadTools: DeadTool[];
|
|
38
70
|
failingTools: FailingTool[];
|
|
39
71
|
recommendations: Recommendation[];
|
|
72
|
+
overview: ToolStatsOverview;
|
|
73
|
+
alerts: Alert[];
|
|
40
74
|
}
|