dsh-tool-stats 0.2.0 → 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/index.d.ts +21 -10
- package/lib/client/index.js +13 -54
- package/lib/client/locales.d.ts +39 -0
- package/lib/client/locales.js +63 -0
- package/lib/client/view.d.ts +23 -1
- package/lib/client/view.js +95 -18
- 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 +263 -39
- package/lib/stats.web.js.map +3 -3
- package/lib/store.d.ts +2 -0
- package/lib/store.js +15 -1
- package/lib/types.d.ts +34 -0
- package/package.json +47 -12
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/index.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser half of dsh-tool-stats:
|
|
2
|
+
* Browser half of dsh-tool-stats: its page inside Settings.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the settings shell is
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
4
|
+
* Two services are injected by their runtime identity, not by the packages that provide
|
|
5
|
+
* them: the settings shell is `slots`, the dictionary registry is `locale`. The label and
|
|
6
|
+
* every string inside the component are translated through this plugin's own namespace,
|
|
7
|
+
* which is why the page follows the UI language instead of the plugin author's.
|
|
8
8
|
*
|
|
9
9
|
* @module client
|
|
10
10
|
*/
|
|
11
11
|
import type { ComponentType } from 'react';
|
|
12
|
+
export declare const inject: string[];
|
|
12
13
|
interface SlotsService {
|
|
13
14
|
inject(name: string, register: () => void): void;
|
|
14
15
|
register(options: {
|
|
@@ -16,11 +17,21 @@ interface SlotsService {
|
|
|
16
17
|
id: string;
|
|
17
18
|
order: number;
|
|
18
19
|
label: () => string;
|
|
19
|
-
|
|
20
|
+
locale: string;
|
|
21
|
+
}, component: ComponentType<{
|
|
22
|
+
t: (key: string, params?: Record<string, unknown>) => string;
|
|
23
|
+
}>): unknown;
|
|
20
24
|
}
|
|
21
|
-
|
|
22
|
-
export declare const inject: string[];
|
|
23
|
-
export declare function apply(ctx: {
|
|
25
|
+
interface ClientContext {
|
|
24
26
|
slots: SlotsService;
|
|
25
|
-
|
|
27
|
+
locale: {
|
|
28
|
+
register(ns: string, dicts: {
|
|
29
|
+
zh: unknown;
|
|
30
|
+
en: unknown;
|
|
31
|
+
}): () => void;
|
|
32
|
+
bind(ns: string): (key: string, params?: Record<string, unknown>) => string;
|
|
33
|
+
};
|
|
34
|
+
effect(callback: () => unknown, label?: string): unknown;
|
|
35
|
+
}
|
|
36
|
+
export declare function apply(ctx: ClientContext): void;
|
|
26
37
|
export {};
|
package/lib/client/index.js
CHANGED
|
@@ -1,65 +1,24 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
1
|
/**
|
|
3
|
-
* Browser half of dsh-tool-stats:
|
|
2
|
+
* Browser half of dsh-tool-stats: its page inside Settings.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* the settings shell is
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
4
|
+
* Two services are injected by their runtime identity, not by the packages that provide
|
|
5
|
+
* them: the settings shell is `slots`, the dictionary registry is `locale`. The label and
|
|
6
|
+
* every string inside the component are translated through this plugin's own namespace,
|
|
7
|
+
* which is why the page follows the UI language instead of the plugin author's.
|
|
9
8
|
*
|
|
10
9
|
* @module client
|
|
11
10
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
const PANEL_PATH = '/api/stats.panel';
|
|
16
|
-
/** Fetch the panel payload; `reload` re-runs the request. */
|
|
17
|
-
function usePanel() {
|
|
18
|
-
const [html, setHtml] = useState(null);
|
|
19
|
-
const [error, setError] = useState(null);
|
|
20
|
-
const [tick, setTick] = useState(0);
|
|
21
|
-
const reload = useCallback(() => setTick((t) => t + 1), []);
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
const controller = new AbortController();
|
|
24
|
-
setError(null);
|
|
25
|
-
fetch(PANEL_PATH, { signal: controller.signal })
|
|
26
|
-
.then(async (response) => {
|
|
27
|
-
if (!response.ok)
|
|
28
|
-
throw new Error(`panel request failed with ${response.status}`);
|
|
29
|
-
return response.json();
|
|
30
|
-
})
|
|
31
|
-
.then((payload) => {
|
|
32
|
-
if (!controller.signal.aborted)
|
|
33
|
-
setHtml(renderPanel(payload));
|
|
34
|
-
})
|
|
35
|
-
.catch((cause) => {
|
|
36
|
-
if (controller.signal.aborted)
|
|
37
|
-
return;
|
|
38
|
-
// Say what failed rather than rendering an empty panel, which would read as
|
|
39
|
-
// "nothing recorded" — a different and wrong answer.
|
|
40
|
-
setError(cause instanceof Error ? cause.message : String(cause));
|
|
41
|
-
});
|
|
42
|
-
return () => controller.abort();
|
|
43
|
-
}, [tick]);
|
|
44
|
-
return { html, error, reload };
|
|
45
|
-
}
|
|
46
|
-
function ToolStatsPage() {
|
|
47
|
-
const { html, error, reload } = usePanel();
|
|
48
|
-
if (error !== null) {
|
|
49
|
-
return (_jsxs("section", { children: [_jsxs("p", { role: "alert", children: ["Tool Stats panel failed to load: ", error] }), _jsx("button", { type: "button", onClick: reload, children: "Retry" })] }));
|
|
50
|
-
}
|
|
51
|
-
if (html === null)
|
|
52
|
-
return _jsx("p", { "aria-live": "polite", children: "Loading the Tool Stats panel\u2026" });
|
|
53
|
-
// The markup is produced by `renderPanel`, which escapes every interpolated value.
|
|
54
|
-
return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
|
|
55
|
-
}
|
|
56
|
-
/** The slots service is what the settings shell exposes. */
|
|
57
|
-
export const inject = ['slots'];
|
|
11
|
+
import { NS, en, zh } from './locales.js';
|
|
12
|
+
import { ToolStatsPanel } from './view.js';
|
|
13
|
+
export const inject = ['slots', 'locale'];
|
|
58
14
|
export function apply(ctx) {
|
|
15
|
+
// `zh` is the key-set source of truth, matching the official client plugins.
|
|
16
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-tool-stats: dictionaries');
|
|
59
17
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
60
18
|
name: 'settings.section',
|
|
61
19
|
id: 'tool-stats',
|
|
62
20
|
order: 43,
|
|
63
|
-
label: () => '
|
|
64
|
-
|
|
21
|
+
label: () => ctx.locale.bind(NS)('nav'),
|
|
22
|
+
locale: NS,
|
|
23
|
+
}, ToolStatsPanel));
|
|
65
24
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dictionaries for the Tool Stats page.
|
|
3
|
+
*
|
|
4
|
+
* `zh` is the key-set source of truth, as in the official client plugins, and `en` is
|
|
5
|
+
* typed against it: a key translated in one language but not the other fails the build
|
|
6
|
+
* instead of silently rendering the raw key.
|
|
7
|
+
*
|
|
8
|
+
* @module client/locales
|
|
9
|
+
*/
|
|
10
|
+
/** Dictionary namespace owned by this plugin. */
|
|
11
|
+
export declare const NS = "toolStats";
|
|
12
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
13
|
+
export declare const zh: {
|
|
14
|
+
nav: string;
|
|
15
|
+
title: string;
|
|
16
|
+
tool: string;
|
|
17
|
+
calls: string;
|
|
18
|
+
failRate: string;
|
|
19
|
+
deadTools: string;
|
|
20
|
+
failingTools: string;
|
|
21
|
+
recommendations: string;
|
|
22
|
+
empty: string;
|
|
23
|
+
refresh: string;
|
|
24
|
+
loading: string;
|
|
25
|
+
failed: string;
|
|
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;
|
|
37
|
+
};
|
|
38
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
39
|
+
export declare const en: typeof zh;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dictionaries for the Tool Stats page.
|
|
3
|
+
*
|
|
4
|
+
* `zh` is the key-set source of truth, as in the official client plugins, and `en` is
|
|
5
|
+
* typed against it: a key translated in one language but not the other fails the build
|
|
6
|
+
* instead of silently rendering the raw key.
|
|
7
|
+
*
|
|
8
|
+
* @module client/locales
|
|
9
|
+
*/
|
|
10
|
+
/** Dictionary namespace owned by this plugin. */
|
|
11
|
+
export const NS = 'toolStats';
|
|
12
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
13
|
+
export const zh = {
|
|
14
|
+
'nav': '工具统计',
|
|
15
|
+
'title': '工具统计',
|
|
16
|
+
'tool': '工具',
|
|
17
|
+
'calls': '调用次数',
|
|
18
|
+
'failRate': '失败率',
|
|
19
|
+
'deadTools': '从未调用的工具',
|
|
20
|
+
'failingTools': '高失败率工具',
|
|
21
|
+
'recommendations': '建议',
|
|
22
|
+
'empty': '还没有记录到工具调用。',
|
|
23
|
+
'refresh': '刷新',
|
|
24
|
+
'loading': '正在加载…',
|
|
25
|
+
'failed': '加载失败',
|
|
26
|
+
'retry': '重试',
|
|
27
|
+
'totalCalls': '总调用',
|
|
28
|
+
'totalFailures': '总失败',
|
|
29
|
+
'sessions': '会话数',
|
|
30
|
+
'avgLatency': '平均延迟',
|
|
31
|
+
'export': '导出 CSV',
|
|
32
|
+
'clear': '清空',
|
|
33
|
+
'confirmClear': '确定清空所有记录?',
|
|
34
|
+
'errorBreakdown': '错误分类',
|
|
35
|
+
'recentCalls': '最近调用',
|
|
36
|
+
'sessionsUnused': '会话未用',
|
|
37
|
+
};
|
|
38
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
39
|
+
export const en = {
|
|
40
|
+
'nav': 'Tool Stats',
|
|
41
|
+
'title': 'Tool Stats',
|
|
42
|
+
'tool': 'Tool',
|
|
43
|
+
'calls': 'Calls',
|
|
44
|
+
'failRate': 'Failure',
|
|
45
|
+
'deadTools': 'Dead tools',
|
|
46
|
+
'failingTools': 'Failing tools',
|
|
47
|
+
'recommendations': 'Recommendations',
|
|
48
|
+
'empty': 'No tool calls recorded yet.',
|
|
49
|
+
'refresh': 'Refresh',
|
|
50
|
+
'loading': 'Loading…',
|
|
51
|
+
'failed': 'Failed to load',
|
|
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',
|
|
63
|
+
};
|
package/lib/client/view.d.ts
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rendering half of the toolStats page.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `index.tsx` so a static render can assert in Node what the page draws —
|
|
5
|
+
* the shipped bundle is a loader factory only a browser can run. Every user-visible
|
|
6
|
+
* string comes from the `t` seat the renderer binds from this plugin's namespace, so the
|
|
7
|
+
* page follows the UI language; no copy is hardcoded here.
|
|
8
|
+
*
|
|
9
|
+
* @module client/view
|
|
10
|
+
*/
|
|
1
11
|
import type { PanelPayload } from '../types.js';
|
|
2
|
-
export
|
|
12
|
+
export type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
13
|
+
export interface PanelProps {
|
|
14
|
+
t: Translate;
|
|
15
|
+
}
|
|
16
|
+
interface PanelState {
|
|
17
|
+
payload: PanelPayload | null;
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function usePanel(): PanelState & {
|
|
21
|
+
reload: () => void;
|
|
22
|
+
};
|
|
23
|
+
export declare function ToolStatsPanel({ t }: PanelProps): import("react").JSX.Element;
|
|
24
|
+
export {};
|
package/lib/client/view.js
CHANGED
|
@@ -1,19 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Pure rendering half of the toolStats page.
|
|
4
|
+
*
|
|
5
|
+
* Separate from `index.tsx` so a static render can assert in Node what the page draws —
|
|
6
|
+
* the shipped bundle is a loader factory only a browser can run. Every user-visible
|
|
7
|
+
* string comes from the `t` seat the renderer binds from this plugin's namespace, so the
|
|
8
|
+
* page follows the UI language; no copy is hardcoded here.
|
|
9
|
+
*
|
|
10
|
+
* @module client/view
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
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' };
|
|
18
|
+
const head = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' };
|
|
19
|
+
const muted = { fontSize: 12, opacity: 0.75 };
|
|
20
|
+
const table = { borderCollapse: 'collapse', width: '100%' };
|
|
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)' };
|
|
22
|
+
const td = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' };
|
|
23
|
+
const list = { margin: 0, paddingLeft: 18, fontSize: 13 };
|
|
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' };
|
|
32
|
+
export function usePanel() {
|
|
33
|
+
const [state, setState] = useState({ payload: null, error: null });
|
|
34
|
+
const [tick, setTick] = useState(0);
|
|
35
|
+
const reload = useCallback(() => setTick((v) => v + 1), []);
|
|
36
|
+
useEffect(() => {
|
|
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();
|
|
47
|
+
}, [tick]);
|
|
48
|
+
return { ...state, reload };
|
|
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
|
+
}
|
|
82
|
+
export function ToolStatsPanel({ t }) {
|
|
83
|
+
const { payload, error, reload } = usePanel();
|
|
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') })] }));
|
|
90
|
+
if (error !== null) {
|
|
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') })] }));
|
|
92
|
+
}
|
|
93
|
+
if (payload === null)
|
|
94
|
+
return _jsx("p", { style: muted, "aria-live": "polite", children: t('loading') });
|
|
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)) })] }))] }));
|
|
19
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
|
@@ -26,69 +26,293 @@ __export(index_exports, {
|
|
|
26
26
|
inject: () => inject
|
|
27
27
|
});
|
|
28
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
|
-
var import_react = require("react");
|
|
30
29
|
|
|
31
|
-
// src/client/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
// src/client/locales.ts
|
|
31
|
+
var NS = "toolStats";
|
|
32
|
+
var zh = {
|
|
33
|
+
"nav": "\u5DE5\u5177\u7EDF\u8BA1",
|
|
34
|
+
"title": "\u5DE5\u5177\u7EDF\u8BA1",
|
|
35
|
+
"tool": "\u5DE5\u5177",
|
|
36
|
+
"calls": "\u8C03\u7528\u6B21\u6570",
|
|
37
|
+
"failRate": "\u5931\u8D25\u7387",
|
|
38
|
+
"deadTools": "\u4ECE\u672A\u8C03\u7528\u7684\u5DE5\u5177",
|
|
39
|
+
"failingTools": "\u9AD8\u5931\u8D25\u7387\u5DE5\u5177",
|
|
40
|
+
"recommendations": "\u5EFA\u8BAE",
|
|
41
|
+
"empty": "\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u5DE5\u5177\u8C03\u7528\u3002",
|
|
42
|
+
"refresh": "\u5237\u65B0",
|
|
43
|
+
"loading": "\u6B63\u5728\u52A0\u8F7D\u2026",
|
|
44
|
+
"failed": "\u52A0\u8F7D\u5931\u8D25",
|
|
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"
|
|
56
|
+
};
|
|
57
|
+
var en = {
|
|
58
|
+
"nav": "Tool Stats",
|
|
59
|
+
"title": "Tool Stats",
|
|
60
|
+
"tool": "Tool",
|
|
61
|
+
"calls": "Calls",
|
|
62
|
+
"failRate": "Failure",
|
|
63
|
+
"deadTools": "Dead tools",
|
|
64
|
+
"failingTools": "Failing tools",
|
|
65
|
+
"recommendations": "Recommendations",
|
|
66
|
+
"empty": "No tool calls recorded yet.",
|
|
67
|
+
"refresh": "Refresh",
|
|
68
|
+
"loading": "Loading\u2026",
|
|
69
|
+
"failed": "Failed to load",
|
|
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"
|
|
81
|
+
};
|
|
43
82
|
|
|
44
|
-
// src/client/
|
|
83
|
+
// src/client/view.tsx
|
|
84
|
+
var import_react = require("react");
|
|
45
85
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
46
86
|
var PANEL_PATH = "/api/stats.panel";
|
|
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" };
|
|
91
|
+
var head = { display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap" };
|
|
92
|
+
var muted = { fontSize: 12, opacity: 0.75 };
|
|
93
|
+
var table = { borderCollapse: "collapse", width: "100%" };
|
|
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)" };
|
|
95
|
+
var td = { padding: "6px 10px 6px 0", fontSize: 13, borderBottom: "0.5px solid rgba(128,128,128,0.18)" };
|
|
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" };
|
|
47
105
|
function usePanel() {
|
|
48
|
-
const [
|
|
49
|
-
const [error, setError] = (0, import_react.useState)(null);
|
|
106
|
+
const [state, setState] = (0, import_react.useState)({ payload: null, error: null });
|
|
50
107
|
const [tick, setTick] = (0, import_react.useState)(0);
|
|
51
|
-
const reload = (0, import_react.useCallback)(() => setTick((
|
|
108
|
+
const reload = (0, import_react.useCallback)(() => setTick((v) => v + 1), []);
|
|
52
109
|
(0, import_react.useEffect)(() => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
fetch(PANEL_PATH, { signal:
|
|
56
|
-
if (!
|
|
57
|
-
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();
|
|
58
115
|
}).then((payload) => {
|
|
59
|
-
if (!
|
|
60
|
-
}).catch((
|
|
61
|
-
if (
|
|
62
|
-
setError(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) });
|
|
63
119
|
});
|
|
64
|
-
return () =>
|
|
120
|
+
return () => c.abort();
|
|
65
121
|
}, [tick]);
|
|
66
|
-
return {
|
|
122
|
+
return { ...state, reload };
|
|
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
|
+
] });
|
|
67
165
|
}
|
|
68
|
-
function
|
|
69
|
-
const
|
|
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
|
+
}
|
|
209
|
+
function ToolStatsPanel({ t }) {
|
|
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]);
|
|
216
|
+
const header = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { style: head, children: [
|
|
217
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 13 }, children: t("title") }),
|
|
218
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
|
|
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") })
|
|
222
|
+
] });
|
|
70
223
|
if (error !== null) {
|
|
71
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("
|
|
72
|
-
|
|
73
|
-
|
|
224
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
|
|
225
|
+
header,
|
|
226
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [
|
|
227
|
+
t("failed"),
|
|
228
|
+
": ",
|
|
74
229
|
error
|
|
75
230
|
] }),
|
|
76
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, children: "
|
|
231
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, style: { ...btn, alignSelf: "flex-start" }, children: t("retry") })
|
|
77
232
|
] });
|
|
78
233
|
}
|
|
79
|
-
if (
|
|
80
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
234
|
+
if (payload === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, "aria-live": "polite", children: t("loading") });
|
|
235
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
|
|
236
|
+
header,
|
|
237
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertBanner, { alerts: payload.alerts, t }),
|
|
238
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Overview, { payload, t }),
|
|
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: [
|
|
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 }),
|
|
242
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("tool") }),
|
|
243
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("calls") }),
|
|
244
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("failRate") }),
|
|
245
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: "p50" }),
|
|
246
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: "p95" })
|
|
247
|
+
] }) }),
|
|
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 }) }),
|
|
251
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }) }),
|
|
252
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: row.invocations }),
|
|
253
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: { ...td, color: row.failureRate > 0.3 ? "#e55" : void 0 }, children: [
|
|
254
|
+
Math.round(row.failureRate * 100),
|
|
255
|
+
"%"
|
|
256
|
+
] }),
|
|
257
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
|
|
258
|
+
row.p50Latency,
|
|
259
|
+
"ms"
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
|
|
262
|
+
row.p95Latency,
|
|
263
|
+
"ms"
|
|
264
|
+
] })
|
|
265
|
+
] }, row.tool) })) })
|
|
266
|
+
] }),
|
|
267
|
+
payload.deadTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
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)) })
|
|
279
|
+
] }),
|
|
280
|
+
payload.failingTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
281
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
282
|
+
"\u26A0\uFE0F ",
|
|
283
|
+
t("failingTools")
|
|
284
|
+
] }),
|
|
285
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: list, children: payload.failingTools.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
|
|
286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.tool }),
|
|
287
|
+
" \xB7 ",
|
|
288
|
+
Math.round(row.failureRate * 100),
|
|
289
|
+
"%",
|
|
290
|
+
row.lastError ? ` \u2014 ${row.lastError.slice(0, 60)}` : ""
|
|
291
|
+
] }, row.tool)) })
|
|
292
|
+
] }),
|
|
293
|
+
payload.recommendations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
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)) })
|
|
299
|
+
] })
|
|
300
|
+
] });
|
|
81
301
|
}
|
|
82
|
-
|
|
302
|
+
|
|
303
|
+
// src/client/index.tsx
|
|
304
|
+
var inject = ["slots", "locale"];
|
|
83
305
|
function apply(ctx) {
|
|
306
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-tool-stats: dictionaries");
|
|
84
307
|
ctx.slots.inject("settings.section", () => ctx.slots.register(
|
|
85
308
|
{
|
|
86
309
|
name: "settings.section",
|
|
87
310
|
id: "tool-stats",
|
|
88
311
|
order: 43,
|
|
89
|
-
label: () => "
|
|
312
|
+
label: () => ctx.locale.bind(NS)("nav"),
|
|
313
|
+
locale: NS
|
|
90
314
|
},
|
|
91
|
-
|
|
315
|
+
ToolStatsPanel
|
|
92
316
|
));
|
|
93
317
|
}
|
|
94
318
|
return module.exports; } });
|
package/lib/stats.web.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/client/index.tsx", "../src/client/view.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * Browser half of dsh-tool-stats: the Tool Stats page inside Settings.\n *\n * Services are named by their runtime identity, not by the package that provides them:\n * the settings shell is the `slots` service and the host bridge is `connection`. A\n * fiber that injects package names never resolves and the plugin stays pending, which\n * the loader reports as \"did not activate\".\n *\n * @module client\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { ComponentType } from 'react'\nimport type { PanelPayload } from '../types.js'\nimport { renderPanel } from './view.js'\n\n/** Path the host half registers on the web connection. */\nconst PANEL_PATH = '/api/stats.panel'\n\ninterface SlotsService {\n inject(name: string, register: () => void): void\n register(options: { name: string; id: string; order: number; label: () => string }, component: ComponentType): unknown\n}\n\n/** Fetch the panel payload; `reload` re-runs the request. */\nfunction usePanel() {\n const [html, setHtml] = useState<string | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [tick, setTick] = useState(0)\n const reload = useCallback(() => setTick((t) => t + 1), [])\n\n useEffect(() => {\n const controller = new AbortController()\n setError(null)\n fetch(PANEL_PATH, { signal: controller.signal })\n .then(async (response) => {\n if (!response.ok) throw new Error(`panel request failed with ${response.status}`)\n return response.json() as Promise<PanelPayload>\n })\n .then((payload) => {\n if (!controller.signal.aborted) setHtml(renderPanel(payload))\n })\n .catch((cause: unknown) => {\n if (controller.signal.aborted) return\n // Say what failed rather than rendering an empty panel, which would read as\n // \"nothing recorded\" \u2014 a different and wrong answer.\n setError(cause instanceof Error ? cause.message : String(cause))\n })\n return () => controller.abort()\n }, [tick])\n\n return { html, error, reload }\n}\n\nfunction ToolStatsPage() {\n const { html, error, reload } = usePanel()\n if (error !== null) {\n return (\n <section>\n <p role=\"alert\">Tool Stats panel failed to load: {error}</p>\n <button type=\"button\" onClick={reload}>Retry</button>\n </section>\n )\n }\n if (html === null) return <p aria-live=\"polite\">Loading the Tool Stats panel\u2026</p>\n // The markup is produced by `renderPanel`, which escapes every interpolated value.\n return <div dangerouslySetInnerHTML={{ __html: html }} />\n}\n\n/** The slots service is what the settings shell exposes. */\nexport const inject = ['slots']\n\nexport function apply(ctx: { slots: SlotsService }): void {\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n {\n name: 'settings.section',\n id: 'tool-stats',\n order: 43,\n label: () => 'Tool Stats',\n },\n ToolStatsPage,\n ))\n}\n", "import type { PanelPayload } from '../types.js';\n\nexport function renderPanel(payload: PanelPayload): string {\n const toolRows = payload.tools\n .map((t) => `<tr><td>${t.tool}</td><td>${t.invocations}</td><td>${Math.round(t.failureRate * 100)}%</td><td>${t.p50Latency}ms</td><td>${t.p95Latency}ms</td></tr>`)\n .join('');\n\n const deadSection = payload.deadTools.length > 0\n ? `<h3>Dead tools</h3><ul>${payload.deadTools.map((d) => `<li>${d.tool}</li>`).join('')}</ul>`\n : '';\n\n const failSection = payload.failingTools.length > 0\n ? `<h3>Failing tools</h3><ul>${payload.failingTools.map((f) => `<li>${f.tool}: ${Math.round(f.failureRate * 100)}%</li>`).join('')}</ul>`\n : '';\n\n const recSection = payload.recommendations.length > 0\n ? `<h3>Recommendations</h3><ul>${payload.recommendations.map((r) => `<li>${r.message}</li>`).join('')}</ul>`\n : '';\n\n return `<div class=\"stats-panel\">\n <h2>Tool Stats</h2>\n ${toolRows ? `<table><thead><tr><th>Tool</th><th>Calls</th><th>Fail</th><th>p50</th><th>p95</th></tr></thead><tbody>${toolRows}</tbody></table>` : '<p>No calls recorded.</p>'}\n ${deadSection}${failSection}${recSection}\n </div>`;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
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 '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
|
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tool-stats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Every tool call is counted, and the ones that never fire are named so you can remove them.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
8
|
-
"bin": {
|
|
8
|
+
"bin": {
|
|
9
|
+
"dsh-tool-stats": "lib/bin.js"
|
|
10
|
+
},
|
|
9
11
|
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"default": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./client": {
|
|
17
|
+
"types": "./lib/client/index.d.ts",
|
|
18
|
+
"default": "./lib/stats.web.js"
|
|
19
|
+
},
|
|
12
20
|
"./package.json": "./package.json"
|
|
13
21
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
|
|
22
|
+
"files": [
|
|
23
|
+
"lib",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"dsh",
|
|
30
|
+
"dsh-plugin",
|
|
31
|
+
"deepseek-harness",
|
|
32
|
+
"tools",
|
|
33
|
+
"analytics",
|
|
34
|
+
"usage"
|
|
35
|
+
],
|
|
16
36
|
"dsh": {
|
|
17
|
-
"bundle": {
|
|
37
|
+
"bundle": {
|
|
38
|
+
"patch": "./cordis.patch.yml"
|
|
39
|
+
},
|
|
18
40
|
"client": {
|
|
19
41
|
"platform": "web",
|
|
20
|
-
"inject": [
|
|
42
|
+
"inject": [
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
44
|
+
"@deepseek-ai/dsh-client-locale",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
46
|
+
]
|
|
21
47
|
}
|
|
22
48
|
},
|
|
23
49
|
"license": "MIT",
|
|
24
50
|
"author": "hj01857655",
|
|
25
|
-
"repository": {
|
|
26
|
-
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/hj01857655/dsh-tool-stats.git"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/hj01857655/dsh-tool-stats/issues"
|
|
57
|
+
},
|
|
27
58
|
"homepage": "https://github.com/hj01857655/dsh-tool-stats#readme",
|
|
28
|
-
"engines": {
|
|
29
|
-
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=20"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
64
|
+
},
|
|
30
65
|
"devDependencies": {
|
|
31
66
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
32
67
|
"@types/node": "^22.20.3",
|