lumencode 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +348 -343
- package/lib/aggregate.js +58 -3
- package/lib/config.js +21 -8
- package/lib/path-utils.js +18 -0
- package/lib/report.js +969 -53
- package/lib/scenario.js +29 -4
- package/lib/server.js +331 -316
- package/package.json +1 -1
- package/public/app.js +232 -17
- package/public/config.js +1 -0
- package/public/export.js +11 -7
- package/public/index.html +77 -16
- package/public/style.css +248 -1
- package/public/utils.js +218 -0
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { COLORS, SCENARIO_COLORS, TEXT, ID, STORAGE } from './config.js';
|
|
2
|
-
import { esc, fmt, fmtShort, destroyChart, destroyAllCharts, getChart, setChart, todayISO, fmtDate } from './utils.js';
|
|
2
|
+
import { esc, fmt, fmtShort, destroyChart, destroyAllCharts, getChart, setChart, todayISO, fmtDate, TOOL_DISPLAY_NAMES, groupMcpByServer, aggregateToolsWithDualCounts } from './utils.js';
|
|
3
3
|
import { createLatestRequestGuard, fetchTools, fetchReport, fetchConfig, saveConfig, fetchDetails, fetchSessions, fetchStepStats, fetchHooksStatus, updateHooks } from './api.js';
|
|
4
4
|
import { renderWorkTypePie, renderModelBars, renderProjectBars, renderTimelineArea, renderCacheStack } from './charts.js';
|
|
5
5
|
import { renderGitInsights, renderLineBlameEvidence } from './git-insights.js';
|
|
@@ -135,6 +135,12 @@ function appState() {
|
|
|
135
135
|
{ l: 'IDLE DAYS', v: '-', s: 'no activity' },
|
|
136
136
|
],
|
|
137
137
|
toolRankData: [],
|
|
138
|
+
toolRankTotal: 0,
|
|
139
|
+
toolRankTab: 'all',
|
|
140
|
+
toolRankTotalCalls: 0,
|
|
141
|
+
toolRankAllTotal: 0,
|
|
142
|
+
toolRankSkillTotal: 0,
|
|
143
|
+
toolRankMcpTotal: 0,
|
|
138
144
|
projectData: [],
|
|
139
145
|
|
|
140
146
|
/* tool summary for rail */
|
|
@@ -310,6 +316,66 @@ function appState() {
|
|
|
310
316
|
if (this.view === 'report') this.loadReportContent();
|
|
311
317
|
},
|
|
312
318
|
|
|
319
|
+
setToolRankTab(tab) {
|
|
320
|
+
this.toolRankTab = tab;
|
|
321
|
+
this._computeToolRank();
|
|
322
|
+
const container = document.getElementById('toolCallsContainer');
|
|
323
|
+
if (container) container.scrollTop = 0;
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
_computeToolRank() {
|
|
327
|
+
const usageStats = this._lastUsageStats || {};
|
|
328
|
+
const tab = this.toolRankTab;
|
|
329
|
+
const getCalls = (val) => typeof val === 'number' ? val : (val.calls || 0);
|
|
330
|
+
const getUses = (val) => typeof val === 'number' ? val : (val.uses || 0);
|
|
331
|
+
const sumCalls = (obj) => Object.values(obj || {}).reduce((s, v) => s + getCalls(v), 0);
|
|
332
|
+
|
|
333
|
+
this.toolRankAllTotal = sumCalls(usageStats.tools);
|
|
334
|
+
this.toolRankSkillTotal = sumCalls(usageStats.skills);
|
|
335
|
+
this.toolRankMcpTotal = sumCalls(usageStats.mcpTools);
|
|
336
|
+
|
|
337
|
+
if (tab === 'all') {
|
|
338
|
+
const dual = aggregateToolsWithDualCounts(usageStats.tools || {});
|
|
339
|
+
const entries = Object.entries(dual).sort((a, b) => b[1].uses - a[1].uses);
|
|
340
|
+
const maxUses = Math.max(...entries.map(([, v]) => v.uses), 1);
|
|
341
|
+
this.toolRankData = entries.map(([name, d]) => ({
|
|
342
|
+
name,
|
|
343
|
+
calls: d.calls,
|
|
344
|
+
uses: d.uses,
|
|
345
|
+
value: d.calls,
|
|
346
|
+
pct: Math.round((d.uses / maxUses) * 100),
|
|
347
|
+
displayName: TOOL_DISPLAY_NAMES[name] || '',
|
|
348
|
+
}));
|
|
349
|
+
this.toolRankTotalCalls = this.toolRankAllTotal;
|
|
350
|
+
} else if (tab === 'skill') {
|
|
351
|
+
const skills = usageStats.skills || {};
|
|
352
|
+
const entries = Object.entries(skills).sort((a, b) => getUses(b[1]) - getUses(a[1]));
|
|
353
|
+
const maxUses = Math.max(...entries.map(([, v]) => getUses(v)), 1);
|
|
354
|
+
this.toolRankData = entries.map(([name, val]) => {
|
|
355
|
+
const calls = getCalls(val);
|
|
356
|
+
const uses = getUses(val);
|
|
357
|
+
return {
|
|
358
|
+
name,
|
|
359
|
+
calls,
|
|
360
|
+
uses,
|
|
361
|
+
value: calls,
|
|
362
|
+
pct: Math.round((uses / maxUses) * 100),
|
|
363
|
+
displayName: '',
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
this.toolRankTotalCalls = this.toolRankSkillTotal;
|
|
367
|
+
} else if (tab === 'mcp') {
|
|
368
|
+
this.toolRankData = groupMcpByServer(usageStats.mcpTools || {});
|
|
369
|
+
this.toolRankTotalCalls = this.toolRankMcpTotal;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.toolRankTotal = this.toolRankData.length;
|
|
373
|
+
let rank = 0;
|
|
374
|
+
for (const item of this.toolRankData) {
|
|
375
|
+
if (!item.isGroup) item.rank = ++rank;
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
313
379
|
/* ── period / date ── */
|
|
314
380
|
setPeriod(p) {
|
|
315
381
|
this.period = p;
|
|
@@ -433,6 +499,7 @@ function appState() {
|
|
|
433
499
|
/* ── render data ── */
|
|
434
500
|
renderData(data) {
|
|
435
501
|
const { usageStats, gitStats, start, end, prevStats, trendData, costBreakdown } = data;
|
|
502
|
+
this._lastUsageStats = usageStats;
|
|
436
503
|
this.hasData = usageStats.requestCount > 0;
|
|
437
504
|
if (!this.hasData) {
|
|
438
505
|
this.kpiData = [
|
|
@@ -526,9 +593,7 @@ function appState() {
|
|
|
526
593
|
this.projectData = projEntries.map(([name, d]) => ({ name: name.length > 20 ? '...' + name.slice(-17) : name, value: d.requests }));
|
|
527
594
|
|
|
528
595
|
/* Tool rank */
|
|
529
|
-
|
|
530
|
-
const maxTool = Math.max(...toolEntries.map(([, v]) => v), 1);
|
|
531
|
-
this.toolRankData = toolEntries.map(([name, value]) => ({ name, value, pct: Math.round((value / maxTool) * 100) }));
|
|
596
|
+
this._computeToolRank();
|
|
532
597
|
|
|
533
598
|
/* Tool rail tokens — only refresh sidebar when viewing all tools */
|
|
534
599
|
if (this.activeTool === 'all') {
|
|
@@ -881,19 +946,167 @@ function showToast(msg) {
|
|
|
881
946
|
}
|
|
882
947
|
|
|
883
948
|
/* ── Settings Modal ── */
|
|
949
|
+
const SCENARIO_LABELS = { coding: '编码', testing: '测试', debugging: '调试', documentation: '文档', review: '审查', planning: '规划', refactoring: '重构' };
|
|
950
|
+
|
|
951
|
+
function renderKeywordsEditor(keywords) {
|
|
952
|
+
const container = document.getElementById('cfgKeywordsEditor');
|
|
953
|
+
if (!container) return;
|
|
954
|
+
container.innerHTML = '';
|
|
955
|
+
for (const [key, label] of Object.entries(SCENARIO_LABELS)) {
|
|
956
|
+
const words = keywords[key] || [];
|
|
957
|
+
const row = document.createElement('div');
|
|
958
|
+
row.className = 'kw-row';
|
|
959
|
+
row.dataset.key = key;
|
|
960
|
+
|
|
961
|
+
const lbl = document.createElement('div');
|
|
962
|
+
lbl.className = 'kw-label';
|
|
963
|
+
lbl.textContent = label;
|
|
964
|
+
|
|
965
|
+
const tags = document.createElement('div');
|
|
966
|
+
tags.className = 'kw-tags';
|
|
967
|
+
for (const w of words) tags.appendChild(makeKwTag(w));
|
|
968
|
+
|
|
969
|
+
const addWrap = document.createElement('div');
|
|
970
|
+
addWrap.className = 'kw-add-row';
|
|
971
|
+
const addBtn = document.createElement('button');
|
|
972
|
+
addBtn.className = 'kw-add-btn';
|
|
973
|
+
addBtn.textContent = '+';
|
|
974
|
+
addBtn.title = '添加关键词';
|
|
975
|
+
addBtn.onclick = () => {
|
|
976
|
+
addWrap.innerHTML = '';
|
|
977
|
+
const inp = document.createElement('input');
|
|
978
|
+
inp.className = 'kw-add-input';
|
|
979
|
+
inp.placeholder = '关键词';
|
|
980
|
+
const ok = document.createElement('button');
|
|
981
|
+
ok.className = 'kw-add-btn';
|
|
982
|
+
ok.textContent = '确定';
|
|
983
|
+
ok.onclick = () => {
|
|
984
|
+
const v = inp.value.trim();
|
|
985
|
+
if (v && !tags.querySelector('[data-word="' + CSS.escape(v) + '"]')) tags.insertBefore(makeKwTag(v), addWrap);
|
|
986
|
+
resetAddBtn();
|
|
987
|
+
};
|
|
988
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter') ok.click(); if (e.key === 'Escape') resetAddBtn(); };
|
|
989
|
+
addWrap.appendChild(inp);
|
|
990
|
+
addWrap.appendChild(ok);
|
|
991
|
+
inp.focus();
|
|
992
|
+
};
|
|
993
|
+
function resetAddBtn() { addWrap.innerHTML = ''; addWrap.appendChild(addBtn); }
|
|
994
|
+
resetAddBtn();
|
|
995
|
+
|
|
996
|
+
row.appendChild(lbl);
|
|
997
|
+
row.appendChild(tags);
|
|
998
|
+
row.appendChild(addWrap);
|
|
999
|
+
container.appendChild(row);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function makeKwTag(word) {
|
|
1004
|
+
const tag = document.createElement('span');
|
|
1005
|
+
tag.className = 'kw-tag';
|
|
1006
|
+
tag.dataset.word = word;
|
|
1007
|
+
tag.textContent = word;
|
|
1008
|
+
const x = document.createElement('span');
|
|
1009
|
+
x.className = 'kw-tag-remove';
|
|
1010
|
+
x.textContent = '×';
|
|
1011
|
+
x.onclick = () => tag.remove();
|
|
1012
|
+
tag.appendChild(x);
|
|
1013
|
+
return tag;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function collectKeywordsFromEditor() {
|
|
1017
|
+
const result = {};
|
|
1018
|
+
const container = document.getElementById('cfgKeywordsEditor');
|
|
1019
|
+
if (!container) return result;
|
|
1020
|
+
for (const row of container.querySelectorAll('.kw-row')) {
|
|
1021
|
+
const key = row.dataset.key;
|
|
1022
|
+
const words = Array.from(row.querySelectorAll('.kw-tag')).map(t => t.dataset.word);
|
|
1023
|
+
if (words.length > 0) result[key] = words;
|
|
1024
|
+
}
|
|
1025
|
+
// 清洗校验:先 trim 再去重、过滤空串、截断超长词、过滤控制字符
|
|
1026
|
+
for (const [key, words] of Object.entries(result)) {
|
|
1027
|
+
result[key] = [...new Set(words.map(w => w.trim()))]
|
|
1028
|
+
.filter(w => w.length > 0 && w.length <= 50)
|
|
1029
|
+
.filter(w => !/[\x00-\x1f\x7f]/.test(w));
|
|
1030
|
+
}
|
|
1031
|
+
return result;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
window.closeSettings = () => {
|
|
1035
|
+
const modal = document.getElementById('settingsModal');
|
|
1036
|
+
if (modal) modal.style.display = 'none';
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
/* ── Advanced Section Toggle ── */
|
|
1040
|
+
window.toggleKeywordsSection = () => {
|
|
1041
|
+
const section = document.getElementById('cfgKeywordsSection');
|
|
1042
|
+
const btn = document.getElementById('cfgKeywordsToggle');
|
|
1043
|
+
if (!section || !btn) return;
|
|
1044
|
+
const isHidden = section.style.display === 'none';
|
|
1045
|
+
section.style.display = isHidden ? 'block' : 'none';
|
|
1046
|
+
btn.classList.toggle('expanded', isHidden);
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
/* ── Path Tag Editor ── */
|
|
1050
|
+
const FOLDER_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
1051
|
+
const CLOSE_ICON = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
1052
|
+
|
|
1053
|
+
function renderPathTags(containerId, paths) {
|
|
1054
|
+
const container = document.getElementById(containerId);
|
|
1055
|
+
if (!container) return;
|
|
1056
|
+
container.innerHTML = '';
|
|
1057
|
+
for (let i = 0; i < paths.length; i++) {
|
|
1058
|
+
const tag = document.createElement('div');
|
|
1059
|
+
tag.className = 'path-tag';
|
|
1060
|
+
tag.innerHTML = `
|
|
1061
|
+
<span class="path-tag-icon">${FOLDER_ICON}</span>
|
|
1062
|
+
<span class="path-tag-text" title="${esc(paths[i])}">${esc(paths[i])}</span>
|
|
1063
|
+
<button class="path-tag-remove" onclick="removePathTag('${containerId}', ${i})" title="删除">${CLOSE_ICON}</button>
|
|
1064
|
+
`;
|
|
1065
|
+
container.appendChild(tag);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function addPathTag(containerId, inputId) {
|
|
1070
|
+
const input = document.getElementById(inputId);
|
|
1071
|
+
const container = document.getElementById(containerId);
|
|
1072
|
+
if (!input || !container) return;
|
|
1073
|
+
const raw = input.value.trim();
|
|
1074
|
+
if (!raw) return;
|
|
1075
|
+
// 支持粘贴多行或多逗号分隔的内容,一次性解析添加
|
|
1076
|
+
const paths = raw.split(/[,,\n\r]+/).map(s => s.trim()).filter(Boolean);
|
|
1077
|
+
const existing = getPathTags(containerId);
|
|
1078
|
+
for (const p of paths) {
|
|
1079
|
+
if (!existing.includes(p)) existing.push(p);
|
|
1080
|
+
}
|
|
1081
|
+
renderPathTags(containerId, existing);
|
|
1082
|
+
input.value = '';
|
|
1083
|
+
input.focus();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function removePathTag(containerId, index) {
|
|
1087
|
+
const paths = getPathTags(containerId);
|
|
1088
|
+
paths.splice(index, 1);
|
|
1089
|
+
renderPathTags(containerId, paths);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function getPathTags(containerId) {
|
|
1093
|
+
const container = document.getElementById(containerId);
|
|
1094
|
+
if (!container) return [];
|
|
1095
|
+
return Array.from(container.querySelectorAll('.path-tag-text')).map(el => el.textContent);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
884
1098
|
window.openSettings = async () => {
|
|
885
1099
|
const modal = document.getElementById('settingsModal');
|
|
1100
|
+
const hint = document.getElementById('cfgSaveHint');
|
|
1101
|
+
if (hint) { hint.textContent = ''; hint.className = ''; }
|
|
886
1102
|
if (modal) modal.style.display = 'flex';
|
|
887
1103
|
try {
|
|
888
1104
|
const cfg = await fetchConfig();
|
|
889
1105
|
const dirEl = document.getElementById('cfgClaudeDir');
|
|
890
|
-
const reposEl = document.getElementById('cfgRepos');
|
|
891
|
-
const excludeEl = document.getElementById('cfgExclude');
|
|
892
|
-
const kwEl = document.getElementById('cfgKeywords');
|
|
893
1106
|
if (dirEl) dirEl.value = cfg.claudeDir || '';
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1107
|
+
renderPathTags('cfgReposTags', cfg.repos || []);
|
|
1108
|
+
renderPathTags('cfgExcludeTags', cfg.excludeProjects || []);
|
|
1109
|
+
renderKeywordsEditor(cfg.scenarioKeywords || {});
|
|
897
1110
|
} catch (err) {
|
|
898
1111
|
showToast('加载配置失败: ' + err.message);
|
|
899
1112
|
}
|
|
@@ -914,19 +1127,21 @@ document.getElementById('welcomeStartBtn')?.addEventListener('click', async () =
|
|
|
914
1127
|
});
|
|
915
1128
|
|
|
916
1129
|
window.saveSettings = async () => {
|
|
917
|
-
|
|
918
|
-
|
|
1130
|
+
const hint = document.getElementById('cfgSaveHint');
|
|
1131
|
+
const scenarioKeywords = collectKeywordsFromEditor();
|
|
919
1132
|
const payload = {
|
|
920
1133
|
claudeDir: document.getElementById('cfgClaudeDir').value.trim(),
|
|
921
|
-
repos:
|
|
922
|
-
excludeProjects:
|
|
1134
|
+
repos: getPathTags('cfgReposTags'),
|
|
1135
|
+
excludeProjects: getPathTags('cfgExcludeTags'),
|
|
923
1136
|
scenarioKeywords,
|
|
924
1137
|
};
|
|
925
1138
|
try {
|
|
926
1139
|
await saveConfig(payload);
|
|
927
|
-
|
|
928
|
-
window.location.reload();
|
|
929
|
-
} catch (err) {
|
|
1140
|
+
if (hint) { hint.textContent = '配置已保存'; hint.className = 'cfg-save-ok'; }
|
|
1141
|
+
setTimeout(() => window.location.reload(), 1200);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
if (hint) { hint.textContent = '保存失败: ' + err.message; hint.className = 'cfg-save-err'; }
|
|
1144
|
+
}
|
|
930
1145
|
};
|
|
931
1146
|
|
|
932
1147
|
/* ── Drill-down global handler ── */
|
package/public/config.js
CHANGED
package/public/export.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { TEXT } from './config.js';
|
|
2
2
|
import { esc, fmt, fmtShort, getChart } from './utils.js';
|
|
3
3
|
|
|
4
|
+
// 工具调用值兼容:{name: number} 或 {name: {calls, uses}}
|
|
5
|
+
const toolCalls = (v) => typeof v === 'number' ? v : (v.calls || 0);
|
|
6
|
+
const toolUses = (v) => typeof v === 'number' ? v : (v.uses || 0);
|
|
7
|
+
|
|
4
8
|
// ── CSV 导出 ──
|
|
5
9
|
export function exportCSV(data, period) {
|
|
6
10
|
if (!data) return;
|
|
@@ -73,11 +77,11 @@ export function exportCSV(data, period) {
|
|
|
73
77
|
lines.push('');
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
const toolEntries = Object.entries(usageStats.tools).sort((a, b) => b[1] - a[1]);
|
|
80
|
+
const toolEntries = Object.entries(usageStats.tools).sort((a, b) => toolCalls(b[1]) - toolCalls(a[1]));
|
|
77
81
|
if (toolEntries.length > 0) {
|
|
78
82
|
lines.push('# 工具使用');
|
|
79
|
-
lines.push('
|
|
80
|
-
for (const [name,
|
|
83
|
+
lines.push('工具,调用次数,使用次数');
|
|
84
|
+
for (const [name, val] of toolEntries) lines.push(`${name},${toolCalls(val)},${toolUses(val)}`);
|
|
81
85
|
lines.push('');
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -121,7 +125,7 @@ export function printReport(data, period) {
|
|
|
121
125
|
|
|
122
126
|
const projRows = Object.entries(usageStats.projects).sort((a, b) => b[1].requests - a[1].requests).map(([n, d]) => [n, d.requests, d.sessions instanceof Set ? d.sessions.size : (d.sessions || 0)]);
|
|
123
127
|
const modelRows = Object.entries(usageStats.models).sort((a, b) => b[1].count - a[1].count).map(([n, d]) => [n, d.count, fmtShort(d.inputTokens), fmtShort(d.outputTokens)]);
|
|
124
|
-
const toolRows = Object.entries(usageStats.tools).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([n,
|
|
128
|
+
const toolRows = Object.entries(usageStats.tools).sort((a, b) => toolCalls(b[1]) - toolCalls(a[1])).slice(0, 10).map(([n, v]) => [n, toolCalls(v), toolUses(v)]);
|
|
125
129
|
const scenarioRows = Object.entries(usageStats.scenarios).sort((a, b) => b[1] - a[1]).map(([n, c]) => [n, c]);
|
|
126
130
|
|
|
127
131
|
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>Claude Code 使用${periodName}</title>
|
|
@@ -156,7 +160,7 @@ th{font-weight:600;background:#f8f9fa}
|
|
|
156
160
|
</div>
|
|
157
161
|
${printTable('项目分布', ['项目', '请求数', '会话数'], projRows)}
|
|
158
162
|
${printTable('模型分布', ['模型', '请求数', '输入', '输出'], modelRows)}
|
|
159
|
-
${printTable('工具使用排行', ['工具', '调用次数'], toolRows)}
|
|
163
|
+
${printTable('工具使用排行', ['工具', '调用次数', '使用次数'], toolRows)}
|
|
160
164
|
${printTable('场景分布', ['场景', '请求数'], scenarioRows)}
|
|
161
165
|
${gitStats && gitStats.commits > 0 ? printTable('Git 代码产出', ['指标', '数值'], (() => {
|
|
162
166
|
const rows = [['提交次数', gitStats.commits], ['新增行数', '+' + fmt(gitStats.linesAdded)], ['删除行数', '-' + fmt(gitStats.linesDeleted)], ['变更文件', gitStats.filesChanged]];
|
|
@@ -229,7 +233,7 @@ export function exportHTML(data, period) {
|
|
|
229
233
|
|
|
230
234
|
const projRows = Object.entries(usageStats.projects).sort((a, b) => b[1].requests - a[1].requests).map(([n, d]) => [n, d.requests, d.sessions instanceof Set ? d.sessions.size : (d.sessions || 0)]);
|
|
231
235
|
const modelRows = Object.entries(usageStats.models).sort((a, b) => b[1].count - a[1].count).map(([n, d]) => [n, d.count, fmtShort(d.inputTokens), fmtShort(d.outputTokens), d.cost ? '$' + d.cost.toFixed(2) : '-']);
|
|
232
|
-
const toolRows = Object.entries(usageStats.tools).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([n,
|
|
236
|
+
const toolRows = Object.entries(usageStats.tools).sort((a, b) => toolCalls(b[1]) - toolCalls(a[1])).slice(0, 10).map(([n, v]) => [n, toolCalls(v), toolUses(v)]);
|
|
233
237
|
const scenarioRows = Object.entries(usageStats.scenarios).sort((a, b) => b[1] - a[1]).map(([n, c]) => [n, c]);
|
|
234
238
|
|
|
235
239
|
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>AI 编码助手使用${periodName}</title>
|
|
@@ -264,7 +268,7 @@ th{font-weight:600;background:#f8f9fa}
|
|
|
264
268
|
</div>
|
|
265
269
|
${printTable('项目分布', ['项目', '请求数', '会话数'], projRows)}
|
|
266
270
|
${printTable('模型分布', ['模型', '请求数', '输入', '输出', '费用'], modelRows)}
|
|
267
|
-
${printTable('工具使用排行', ['工具', '调用次数'], toolRows)}
|
|
271
|
+
${printTable('工具使用排行', ['工具', '调用次数', '使用次数'], toolRows)}
|
|
268
272
|
${printTable('场景分布', ['场景', '请求数'], scenarioRows)}
|
|
269
273
|
${data.costBreakdown?.models?.length ? printTable('模型费用', ['模型', '费用', '计费方式', '请求数'], data.costBreakdown.models.map(m => [m.name, '$' + (m.cost || 0).toFixed(2), m.mode === 'actual' ? '实际' : m.mode === 'estimated' ? '估算' : '未知', m.requests])) : ''}
|
|
270
274
|
${gitStats && gitStats.commits > 0 ? printTable('Git 代码产出', ['指标', '数值'], [['提交次数', gitStats.commits], ['新增行数', '+' + fmt(gitStats.linesAdded)], ['删除行数', '-' + fmt(gitStats.linesDeleted)], ['变更文件', gitStats.filesChanged]]) : ''}
|
package/public/index.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
@@ -509,15 +509,31 @@
|
|
|
509
509
|
<h2 class="section-head-title">工具调用排行</h2>
|
|
510
510
|
<span class="label section-head-en">/ TOOL CALLS</span>
|
|
511
511
|
<span class="section-head-line"></span>
|
|
512
|
+
<div style="display:flex;align-items:center;gap:8px;margin-left:auto;">
|
|
513
|
+
<div style="display:flex;gap:0;border:1px solid var(--border);border-radius:4px;overflow:hidden;">
|
|
514
|
+
<button class="period-btn" :class="toolRankTab === 'all' ? 'active' : ''" @click="setToolRankTab('all')" style="border:none;font-size:10px;padding:2px 8px;" x-text="'全部 ' + toolRankAllTotal"></button>
|
|
515
|
+
<button class="period-btn" :class="toolRankTab === 'skill' ? 'active' : ''" @click="setToolRankTab('skill')" style="border:none;font-size:10px;padding:2px 8px;" x-text="'Skill ' + toolRankSkillTotal"></button>
|
|
516
|
+
<button class="period-btn" :class="toolRankTab === 'mcp' ? 'active' : ''" @click="setToolRankTab('mcp')" style="border:none;font-size:10px;padding:2px 8px;" x-text="'MCP ' + toolRankMcpTotal"></button>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
512
519
|
</div>
|
|
513
520
|
<div class="card" style="padding:24px;">
|
|
514
|
-
<div id="toolCallsContainer" style="display:flex;flex-direction:column;gap:12px;">
|
|
515
|
-
<template x-for="(t, i) in toolRankData" :key="t.name">
|
|
516
|
-
<div
|
|
517
|
-
|
|
518
|
-
<
|
|
519
|
-
|
|
520
|
-
|
|
521
|
+
<div id="toolCallsContainer" style="display:flex;flex-direction:column;gap:12px;max-height:420px;overflow-y:auto;padding-right:4px;">
|
|
522
|
+
<template x-for="(t, i) in toolRankData" :key="toolRankTab + '-' + t.name + '-' + i">
|
|
523
|
+
<div style="display:contents">
|
|
524
|
+
<!-- MCP 分组行 -->
|
|
525
|
+
<div x-show="t.isGroup" class="tool-rank-group">
|
|
526
|
+
<span class="font-mono" x-text="t.name"></span>
|
|
527
|
+
</div>
|
|
528
|
+
<!-- 普通排行行 -->
|
|
529
|
+
<div x-show="!t.isGroup" class="tool-rank" :class="toolRankTab === 'all' ? 'has-note' : ''">
|
|
530
|
+
<span class="font-mono" style="font-size:10px;opacity:0.4" x-text="String(t.rank || (i+1)).padStart(2,'0')"></span>
|
|
531
|
+
<span class="font-mono" style="font-size:12px" x-text="t.name" :title="t.name"></span>
|
|
532
|
+
<div class="bar-track"><div class="bar-fill" :style="'width:' + t.pct + '%;background:' + (t.rank === 1 ? colors.rust : 'var(--foreground)')"></div></div>
|
|
533
|
+
<span class="font-mono text-right" style="font-size:11px" x-text="t.calls === t.uses ? t.calls : t.calls + '/' + t.uses" :title="t.calls === t.uses ? '使用次数:' + t.calls : '调用次数 / 使用次数:' + t.calls + ' / ' + t.uses"></span>
|
|
534
|
+
<!-- 通俗备注(仅全部工具 Tab)-->
|
|
535
|
+
<span x-show="toolRankTab === 'all' && t.displayName" style="font-size:11px;opacity:0.45;white-space:nowrap;" x-text="t.displayName"></span>
|
|
536
|
+
</div>
|
|
521
537
|
</div>
|
|
522
538
|
</template>
|
|
523
539
|
</div>
|
|
@@ -546,6 +562,7 @@
|
|
|
546
562
|
<div style="display:flex;border:1px solid var(--border);border-radius:6px;overflow:hidden;">
|
|
547
563
|
<button class="period-btn" :class="reportLevel === 'detailed' ? 'active' : ''" @click="setReportLevel('detailed')" style="border-left:none">详细 Detail</button>
|
|
548
564
|
<button class="period-btn" :class="reportLevel === 'brief' ? 'active' : ''" @click="setReportLevel('brief')">简略 Brief</button>
|
|
565
|
+
<button class="period-btn" :class="reportLevel === 'boss' ? 'active' : ''" @click="setReportLevel('boss')">汇报 Boss</button>
|
|
549
566
|
</div>
|
|
550
567
|
<button class="btn btn-outline" @click="copyReport()" style="display:inline-flex;align-items:center;gap:6px;">
|
|
551
568
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
@@ -713,21 +730,64 @@
|
|
|
713
730
|
|
|
714
731
|
<!-- ── Settings Modal ── -->
|
|
715
732
|
<div id="settingsModal" class="modal-overlay" style="display:none;">
|
|
716
|
-
<div class="modal-backdrop" onclick="
|
|
717
|
-
<div class="modal-panel">
|
|
733
|
+
<div class="modal-backdrop" onclick="closeSettings()"></div>
|
|
734
|
+
<div class="modal-panel" style="max-width:580px;">
|
|
718
735
|
<div class="modal-header">
|
|
719
736
|
<h3 style="font-size:15px;font-weight:500;">配置</h3>
|
|
720
|
-
<button class="rail-btn-icon" onclick="
|
|
737
|
+
<button class="rail-btn-icon" onclick="closeSettings()" style="color:var(--foreground);opacity:0.65;">
|
|
721
738
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
722
739
|
</button>
|
|
723
740
|
</div>
|
|
724
741
|
<div class="modal-body">
|
|
725
|
-
|
|
726
|
-
<div class="
|
|
727
|
-
<div class="form-group"
|
|
728
|
-
|
|
742
|
+
<!-- 数据源 -->
|
|
743
|
+
<div class="cfg-section-title">数据源</div>
|
|
744
|
+
<div class="form-group">
|
|
745
|
+
<label class="form-label">Claude 日志目录</label>
|
|
746
|
+
<input type="text" id="cfgClaudeDir" class="form-input" placeholder="例如: C:/Users/xxx/.claude">
|
|
747
|
+
<p class="form-hint">Claude Code 会话日志的存放路径,通常是 ~/.claude</p>
|
|
748
|
+
</div>
|
|
749
|
+
<!-- 代码仓库 -->
|
|
750
|
+
<div class="cfg-section-title" style="margin-top:28px;">代码仓库</div>
|
|
751
|
+
<div class="form-group">
|
|
752
|
+
<label class="form-label">项目路径</label>
|
|
753
|
+
<div id="cfgReposTags" class="path-tags"></div>
|
|
754
|
+
<div class="path-add-box">
|
|
755
|
+
<span class="path-add-icon">
|
|
756
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
757
|
+
</span>
|
|
758
|
+
<input type="text" id="cfgReposInput" class="path-add-input" placeholder="粘贴或输入绝对路径..." onkeydown="if(event.key==='Enter')addPathTag('cfgReposTags','cfgReposInput')">
|
|
759
|
+
<button class="path-add-btn" onclick="addPathTag('cfgReposTags','cfgReposInput')">添加</button>
|
|
760
|
+
</div>
|
|
761
|
+
<p class="form-hint">按回车或点击添加,路径可单独删除</p>
|
|
762
|
+
</div>
|
|
763
|
+
<div class="form-group">
|
|
764
|
+
<label class="form-label">排除项目 <span style="opacity:0.4;font-weight:400;">(可选)</span></label>
|
|
765
|
+
<div id="cfgExcludeTags" class="path-tags"></div>
|
|
766
|
+
<div class="path-add-box">
|
|
767
|
+
<span class="path-add-icon">
|
|
768
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
769
|
+
</span>
|
|
770
|
+
<input type="text" id="cfgExcludeInput" class="path-add-input" placeholder="输入要排除的项目名..." onkeydown="if(event.key==='Enter')addPathTag('cfgExcludeTags','cfgExcludeInput')">
|
|
771
|
+
<button class="path-add-btn" onclick="addPathTag('cfgExcludeTags','cfgExcludeInput')">添加</button>
|
|
772
|
+
</div>
|
|
773
|
+
<p class="form-hint">按回车或点击添加,项目名称可单独删除</p>
|
|
774
|
+
</div>
|
|
775
|
+
<!-- 场景分类(默认折叠) -->
|
|
776
|
+
<div style="margin-top:28px;">
|
|
777
|
+
<button id="cfgKeywordsToggle" class="cfg-advanced-toggle" onclick="toggleKeywordsSection()">
|
|
778
|
+
<span>场景分类</span>
|
|
779
|
+
<svg id="cfgKeywordsArrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
780
|
+
</button>
|
|
781
|
+
<div id="cfgKeywordsSection" class="cfg-advanced-body" style="display:none;">
|
|
782
|
+
<p class="form-hint" style="margin-bottom:12px;">根据工具调用中的关键词自动判断工作场景,点击可编辑</p>
|
|
783
|
+
<div id="cfgKeywordsEditor"></div>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
<div class="modal-footer">
|
|
788
|
+
<span id="cfgSaveHint" style="font-size:12px;margin-right:auto;"></span>
|
|
789
|
+
<button class="btn btn-primary" onclick="saveSettings()">保存配置</button>
|
|
729
790
|
</div>
|
|
730
|
-
<div class="modal-footer"><button class="btn btn-primary" onclick="saveSettings()">保存</button></div>
|
|
731
791
|
</div>
|
|
732
792
|
</div>
|
|
733
793
|
|
|
@@ -795,3 +855,4 @@
|
|
|
795
855
|
<script type="module" src="/app.js"></script>
|
|
796
856
|
</body>
|
|
797
857
|
</html>
|
|
858
|
+
|