ccus-cli 0.2.25 → 0.2.27
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/README.md +4 -2
- package/dist/cli.js +1 -1
- package/dist/lib/aggregate-dashboard.js +41 -8
- package/dist/lib/aggregate.js +1 -0
- package/dist/lib/dashboard.js +3 -4
- package/dist/lib/export.js +4 -2
- package/dist/lib/weekly-score.js +50 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
|
|
23
23
|
个人和聚合面板均提供「周评分」:默认 `50 × √(周消息数 / 200) + 50 × √(周累计额度百分点 / 70)`,消息和额度均合计 Claude + Codex。两项达到基准得 100 分,分数不封顶。
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
个人和团队面板提供「出勤天数」选择,默认 5 天,可选 0.5~7 天,步进 0.5 天,切换后立即更新评分。消息和额度达标线按 `出勤天数 / 5` 调整,原始统计值不变;例如 3 天对应 120 条消息、42 个额度百分点。团队顶部选择批量设置所有成员,每行可单独调整,该成员所展示的各周共用所选天数;刷新页面恢复 5 天。点击「导出周统计 CSV」下载按当前各成员出勤天数评分的 `weekly.csv`,保留现有列,多周逐周导出。命令行导出仍按默认 5 天评分。
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
权重和标准 5 天基准集中在 `src/lib/weekly-score.ts` 的 `DEFAULT_SCORE_SETTINGS` 修改:`messageWeight` 为消息占比(额度占比为 100 减去消息占比),`messageBaseline` 为周消息基准,`quotaBaseline` 为周额度基准。修改后重新构建并重启面板服务。
|
|
28
|
+
|
|
29
|
+
评分按当前查询范围逐周计算,未满一周不折算;查看本周评分请使用 `ccus dashboard serve --range this-week`。聚合面板在「多人对比」中显示周评分,多周取逐周评分的平均值。额度缺失显示 `--`(消息占比为 100% 时可单独评分)。`weekly.csv` 末尾包含 `weeklyScore`、`attendanceDays` 两列:评分与看板共用公式,保留最多 1 位小数,不可用时留空;出勤天数为 0.5~7、步进 0.5,看板导出使用各成员当前选择,命令行默认 5 天。bundle、`daily.csv` 和 `detail.csv` 不变。
|
|
28
30
|
|
|
29
31
|
## 安装
|
|
30
32
|
|
package/dist/cli.js
CHANGED
|
@@ -613,7 +613,7 @@ async function handleAggregate(options) {
|
|
|
613
613
|
(0, debug_1.debugLog)("aggregate", "bundles loaded", { bundleCount: bundles.length, detailRows: detailRows.length, dailyRows: dailyRows.length, weeklyRows: weeklyRows.length });
|
|
614
614
|
const detailCsv = (0, export_1.buildAggregatedDetailCsv)(detailRows);
|
|
615
615
|
const dailyCsv = (0, export_1.buildAggregatedDailyCsv)(dailyRows);
|
|
616
|
-
const weeklyCsv = (0, export_1.buildAggregatedWeeklyCsv)(weeklyRows);
|
|
616
|
+
const weeklyCsv = (0, export_1.buildAggregatedWeeklyCsv)(weeklyRows, 5);
|
|
617
617
|
const detailPath = node_path_1.default.join(outputDir, "detail.csv");
|
|
618
618
|
const dailyPath = node_path_1.default.join(outputDir, "daily.csv");
|
|
619
619
|
const weeklyPath = node_path_1.default.join(outputDir, "weekly.csv");
|
|
@@ -9,6 +9,7 @@ const time_1 = require("./time");
|
|
|
9
9
|
const api_equivalent_cost_1 = require("./api-equivalent-cost");
|
|
10
10
|
const api_pricing_table_1 = require("./api-pricing-table");
|
|
11
11
|
const weekly_score_1 = require("./weekly-score");
|
|
12
|
+
const export_1 = require("./export");
|
|
12
13
|
/** 折线 / 图例统一的调色板,保证同一个人在不同图表里颜色一致。 */
|
|
13
14
|
const CHART_PALETTE = ["#5eead4", "#f59e0b", "#a855f7", "#22c55e", "#f87171", "#60a5fa", "#fbbf24", "#34d399"];
|
|
14
15
|
/**
|
|
@@ -433,21 +434,16 @@ function renderPeopleLeaderboard(people, weeklyRows) {
|
|
|
433
434
|
const scores = new Map();
|
|
434
435
|
for (const row of weeklyRows) {
|
|
435
436
|
const values = scores.get(row.personKey) ?? [];
|
|
436
|
-
values.push(
|
|
437
|
+
values.push(row);
|
|
437
438
|
scores.set(row.personKey, values);
|
|
438
439
|
}
|
|
439
|
-
const scoreText = (personKey) => {
|
|
440
|
-
const values = scores.get(personKey) ?? [];
|
|
441
|
-
if (values.length === 0 || values.some(value => value === null))
|
|
442
|
-
return "--";
|
|
443
|
-
return (values.reduce((sum, value) => sum + (value ?? 0), 0) / values.length).toFixed(1);
|
|
444
|
-
};
|
|
445
440
|
const rows = people
|
|
446
441
|
.map((person, index) => `
|
|
447
442
|
<tr>
|
|
448
443
|
<td class="rank">${index + 1}</td>
|
|
449
444
|
<td><strong>${escapeHtml(person.personKey)}</strong></td>
|
|
450
|
-
<td><strong>${
|
|
445
|
+
<td><strong>${(0, weekly_score_1.renderAttendanceScore)(scores.get(person.personKey) ?? [])}</strong></td>
|
|
446
|
+
<td>${(0, weekly_score_1.renderAttendanceControl)(person.personKey)}</td>
|
|
451
447
|
<td>${formatNumber(person.userMessageCount)}</td>
|
|
452
448
|
<td>${formatTokensM(person.inputTokens)}</td>
|
|
453
449
|
<td>${formatTokensM(person.outputTokens)}</td>
|
|
@@ -468,6 +464,8 @@ function renderPeopleLeaderboard(people, weeklyRows) {
|
|
|
468
464
|
<h2>多人对比</h2>
|
|
469
465
|
</div>
|
|
470
466
|
<p class="muted">按用户消息数降序排列;评分按周计算,多周取平均,缺失额度显示 --。</p>
|
|
467
|
+
${(0, weekly_score_1.renderAttendanceControl)()}
|
|
468
|
+
<button type="button" id="export-weekly-csv">导出周统计 CSV</button>
|
|
471
469
|
</div>
|
|
472
470
|
<div class="table-wrap">
|
|
473
471
|
<table>
|
|
@@ -476,6 +474,7 @@ function renderPeopleLeaderboard(people, weeklyRows) {
|
|
|
476
474
|
<th>#</th>
|
|
477
475
|
<th>personKey</th>
|
|
478
476
|
<th>周评分</th>
|
|
477
|
+
<th>出勤天数</th>
|
|
479
478
|
<th>消息</th>
|
|
480
479
|
<th>Input tokens</th>
|
|
481
480
|
<th>Output tokens</th>
|
|
@@ -494,6 +493,38 @@ function renderPeopleLeaderboard(people, weeklyRows) {
|
|
|
494
493
|
</section>
|
|
495
494
|
`;
|
|
496
495
|
}
|
|
496
|
+
function weeklyCsvDownloadScript(rows) {
|
|
497
|
+
const header = (0, export_1.buildAggregatedWeeklyCsv)([]);
|
|
498
|
+
const data = rows.map(row => ({
|
|
499
|
+
personKey: row.personKey,
|
|
500
|
+
csv: weekly_score_1.ATTENDANCE_DAYS.map(days => (0, export_1.buildAggregatedWeeklyCsv)([row], days).slice(header.length + 1)),
|
|
501
|
+
}));
|
|
502
|
+
// 防止用户字段闭合内联脚本;CSV 转义和公式防护复用导出模块。
|
|
503
|
+
const serialized = JSON.stringify({ header, rows: data }).replaceAll("<", "\\u003c");
|
|
504
|
+
return `<script>
|
|
505
|
+
(() => {
|
|
506
|
+
const button = document.getElementById('export-weekly-csv');
|
|
507
|
+
if (!button) return;
|
|
508
|
+
const data = ${serialized};
|
|
509
|
+
button.addEventListener('click', () => {
|
|
510
|
+
const attendance = new Map();
|
|
511
|
+
document.querySelectorAll('[data-attendance-person]').forEach(control => {
|
|
512
|
+
const days = Number(control.value);
|
|
513
|
+
attendance.set(control.dataset.attendancePerson, Number.isInteger(days * 2) && days >= 0.5 && days <= 7 ? days : 5);
|
|
514
|
+
});
|
|
515
|
+
const csv = [data.header, ...data.rows.map(row => row.csv[(attendance.get(row.personKey) ?? 5) * 2 - 1])].join('\\n');
|
|
516
|
+
const url = URL.createObjectURL(new Blob(['\\uFEFF', csv], { type: 'text/csv;charset=utf-8' }));
|
|
517
|
+
const link = document.createElement('a');
|
|
518
|
+
link.href = url;
|
|
519
|
+
link.download = 'weekly.csv';
|
|
520
|
+
document.body.appendChild(link);
|
|
521
|
+
link.click();
|
|
522
|
+
link.remove();
|
|
523
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
524
|
+
});
|
|
525
|
+
})();
|
|
526
|
+
</script>`;
|
|
527
|
+
}
|
|
497
528
|
/** 把 daily 行按 personKey × date 排成一个矩阵表。 */
|
|
498
529
|
function renderDailyMatrix(people, dailyIndex, dateAxis) {
|
|
499
530
|
if (people.length === 0 || dateAxis.length === 0) {
|
|
@@ -816,6 +847,8 @@ function buildAggregateDashboardHtml(detailRows, dailyRows, weeklyRows, generate
|
|
|
816
847
|
${renderDailyMatrix(people, dailyIndex, dateAxis)}
|
|
817
848
|
${renderWeeklyTable(weeklyRows)}
|
|
818
849
|
</main>
|
|
850
|
+
${(0, weekly_score_1.attendanceScoreScript)()}
|
|
851
|
+
${weeklyCsvDownloadScript(weeklyRows)}
|
|
819
852
|
${(0, chart_assets_1.uplotBodyScripts)()}
|
|
820
853
|
</body>
|
|
821
854
|
</html>`;
|
package/dist/lib/aggregate.js
CHANGED
|
@@ -697,6 +697,7 @@ function fallbackWeeklyUsage(days) {
|
|
|
697
697
|
/**
|
|
698
698
|
* 展开 weekly.csv:不同机器的同人同天数据已在 selectDailyRepresentatives 层按 sessionId 去重分组,
|
|
699
699
|
* 这里直接按 (person, 周) 把所有代表的 token / 计数累加上卷,usage 从该周所有代表事件重算。
|
|
700
|
+
* 出勤天数由 CSV 导出层提供,不从原始事件推算。
|
|
700
701
|
*/
|
|
701
702
|
function buildAggregatedWeeklyRows(bundles) {
|
|
702
703
|
const repsMap = selectDailyRepresentatives(bundles);
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -325,9 +325,6 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
|
|
|
325
325
|
const combinedUserMessages = totalUserMessages + codexTotalUserMessages;
|
|
326
326
|
const scoreWeek = (0, time_1.localDateKey)((0, time_1.resolveRange)("this-week", end, (0, time_1.parseDayStart)(dayStart)).start);
|
|
327
327
|
const scoreRow = buildPersonalWeeklyScores(events, dailyUserMessages, dayStart).find(row => row.week === scoreWeek);
|
|
328
|
-
const weeklyScore = scoreRow
|
|
329
|
-
? (0, weekly_score_1.calculateWeeklyScore)(scoreRow.userMessageCount, scoreRow.sevenDayCumulativeUsagePct, weekly_score_1.DEFAULT_SCORE_SETTINGS)
|
|
330
|
-
: null;
|
|
331
328
|
return `<!doctype html>
|
|
332
329
|
<html lang="zh-CN">
|
|
333
330
|
<head>
|
|
@@ -507,7 +504,8 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
|
|
|
507
504
|
<section class="stats">
|
|
508
505
|
<article class="panel stat-card" id="weekly-score">
|
|
509
506
|
<h2>周评分</h2>
|
|
510
|
-
<p class="stat-value">${
|
|
507
|
+
<p class="stat-value">${(0, weekly_score_1.renderAttendanceScore)(scoreRow ? [scoreRow] : [])}</p>
|
|
508
|
+
${(0, weekly_score_1.renderAttendanceControl)()}
|
|
511
509
|
<p class="stat-note">${scoreWeek} 当周 · 当前查询范围内数据</p>
|
|
512
510
|
</article>
|
|
513
511
|
<article class="panel stat-card">
|
|
@@ -527,6 +525,7 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
|
|
|
527
525
|
${renderDailyMessages(dailyUserMessages)}
|
|
528
526
|
${renderRecentEvents(events)}
|
|
529
527
|
</main>
|
|
528
|
+
${(0, weekly_score_1.attendanceScoreScript)()}
|
|
530
529
|
${(0, chart_assets_1.uplotBodyScripts)()}
|
|
531
530
|
</body>
|
|
532
531
|
</html>`;
|
package/dist/lib/export.js
CHANGED
|
@@ -249,7 +249,7 @@ function buildAggregatedDailyCsv(rows) {
|
|
|
249
249
|
return [header.join(","), ...lines].join("\n");
|
|
250
250
|
}
|
|
251
251
|
/** 多人汇总的 weekly.csv。 */
|
|
252
|
-
function buildAggregatedWeeklyCsv(rows) {
|
|
252
|
+
function buildAggregatedWeeklyCsv(rows, attendanceDays = 5) {
|
|
253
253
|
const header = [
|
|
254
254
|
"personKey",
|
|
255
255
|
"week",
|
|
@@ -269,9 +269,10 @@ function buildAggregatedWeeklyCsv(rows) {
|
|
|
269
269
|
"estimatedApiEquivalentCostUsd",
|
|
270
270
|
"pricingCatalogVersion",
|
|
271
271
|
"weeklyScore",
|
|
272
|
+
"attendanceDays",
|
|
272
273
|
];
|
|
273
274
|
const lines = rows.map((row) => {
|
|
274
|
-
const score = (0, weekly_score_1.calculateWeeklyScore)(row.userMessageCount, row.sevenDayCumulativeUsagePct, weekly_score_1.DEFAULT_SCORE_SETTINGS);
|
|
275
|
+
const score = (0, weekly_score_1.calculateWeeklyScore)(row.userMessageCount, row.sevenDayCumulativeUsagePct, weekly_score_1.DEFAULT_SCORE_SETTINGS, attendanceDays);
|
|
275
276
|
return toCsvLine([
|
|
276
277
|
row.personKey,
|
|
277
278
|
row.week,
|
|
@@ -291,6 +292,7 @@ function buildAggregatedWeeklyCsv(rows) {
|
|
|
291
292
|
formatApiEquivalentCost(row.estimatedApiEquivalentCostUsd),
|
|
292
293
|
row.pricingCatalogVersion,
|
|
293
294
|
score === null ? null : Number(score.toFixed(1)),
|
|
295
|
+
attendanceDays,
|
|
294
296
|
]);
|
|
295
297
|
});
|
|
296
298
|
return [header.join(","), ...lines].join("\n");
|
package/dist/lib/weekly-score.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_SCORE_SETTINGS = void 0;
|
|
3
|
+
exports.ATTENDANCE_DAYS = exports.DEFAULT_SCORE_SETTINGS = void 0;
|
|
4
4
|
exports.calculateWeeklyScore = calculateWeeklyScore;
|
|
5
|
+
exports.renderAttendanceControl = renderAttendanceControl;
|
|
6
|
+
exports.renderAttendanceScore = renderAttendanceScore;
|
|
7
|
+
exports.attendanceScoreScript = attendanceScoreScript;
|
|
5
8
|
exports.renderWeeklyScores = renderWeeklyScores;
|
|
6
9
|
exports.DEFAULT_SCORE_SETTINGS = {
|
|
7
10
|
messageWeight: 50,
|
|
8
11
|
messageBaseline: 200,
|
|
9
12
|
quotaBaseline: 70,
|
|
10
13
|
};
|
|
11
|
-
|
|
14
|
+
exports.ATTENDANCE_DAYS = Array.from({ length: 14 }, (_, index) => (index + 1) / 2);
|
|
15
|
+
function calculateWeeklyScore(messages, quota, settings, attendanceDays = 5) {
|
|
12
16
|
const { messageWeight, messageBaseline, quotaBaseline } = settings;
|
|
13
|
-
if (!Number.
|
|
17
|
+
if (!Number.isInteger(attendanceDays * 2) || attendanceDays < 0.5 || attendanceDays > 7
|
|
18
|
+
|| !Number.isFinite(messageWeight) || messageWeight < 0 || messageWeight > 100
|
|
14
19
|
|| !Number.isFinite(messageBaseline) || messageBaseline <= 0
|
|
15
20
|
|| !Number.isFinite(quotaBaseline) || quotaBaseline <= 0
|
|
16
21
|
|| !Number.isFinite(messages) || messages < 0
|
|
@@ -18,8 +23,48 @@ function calculateWeeklyScore(messages, quota, settings) {
|
|
|
18
23
|
return null;
|
|
19
24
|
if (quota === null && messageWeight < 100)
|
|
20
25
|
return null;
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
const attendanceRatio = attendanceDays / 5;
|
|
27
|
+
return messageWeight * Math.sqrt(messages / (messageBaseline * attendanceRatio))
|
|
28
|
+
+ (100 - messageWeight) * Math.sqrt((quota ?? 0) / (quotaBaseline * attendanceRatio));
|
|
29
|
+
}
|
|
30
|
+
function renderAttendanceControl(personKey) {
|
|
31
|
+
const attributes = personKey === undefined ? 'id="attendance-days"' : `data-attendance-person="${escapeHtml(personKey)}" aria-label="出勤天数"`;
|
|
32
|
+
const select = `<select ${attributes}>${exports.ATTENDANCE_DAYS.map(days => `<option value="${days}"${days === 5 ? " selected" : ""}>${days} 天</option>`).join("")}</select>`;
|
|
33
|
+
return personKey === undefined ? `<label>出勤天数 ${select}</label>` : select;
|
|
34
|
+
}
|
|
35
|
+
/** 多周逐周评分后取平均;浏览器切换预先按各出勤天数算好的结果。 */
|
|
36
|
+
function renderAttendanceScore(rows) {
|
|
37
|
+
const scores = exports.ATTENDANCE_DAYS.map(days => {
|
|
38
|
+
const values = rows.map(row => calculateWeeklyScore(row.userMessageCount, row.sevenDayCumulativeUsagePct, exports.DEFAULT_SCORE_SETTINGS, days));
|
|
39
|
+
if (!values.length || values.some(value => value === null))
|
|
40
|
+
return "--";
|
|
41
|
+
return (values.reduce((sum, value) => sum + (value ?? 0), 0) / values.length).toFixed(1);
|
|
42
|
+
});
|
|
43
|
+
return `<span data-attendance-scores="${escapeHtml(JSON.stringify(scores))}">${scores[9]}</span>`;
|
|
44
|
+
}
|
|
45
|
+
function attendanceScoreScript() {
|
|
46
|
+
return `<script>
|
|
47
|
+
(() => {
|
|
48
|
+
const select = document.getElementById('attendance-days');
|
|
49
|
+
if (!select) return;
|
|
50
|
+
select.addEventListener('change', () => {
|
|
51
|
+
const days = Number(select.value);
|
|
52
|
+
if (!Number.isInteger(days * 2) || days < 0.5 || days > 7) return;
|
|
53
|
+
document.querySelectorAll('[data-attendance-scores]').forEach(element => {
|
|
54
|
+
element.textContent = JSON.parse(element.dataset.attendanceScores)[days * 2 - 1];
|
|
55
|
+
});
|
|
56
|
+
document.querySelectorAll('[data-attendance-person]').forEach(element => { element.value = select.value; });
|
|
57
|
+
});
|
|
58
|
+
document.querySelectorAll('[data-attendance-person]').forEach(control => {
|
|
59
|
+
control.addEventListener('change', () => {
|
|
60
|
+
const days = Number(control.value);
|
|
61
|
+
if (!Number.isInteger(days * 2) || days < 0.5 || days > 7) return;
|
|
62
|
+
const score = control.closest('tr').querySelector('[data-attendance-scores]');
|
|
63
|
+
score.textContent = JSON.parse(score.dataset.attendanceScores)[days * 2 - 1];
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
})();
|
|
67
|
+
</script>`;
|
|
23
68
|
}
|
|
24
69
|
function escapeHtml(value) {
|
|
25
70
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|