ccus-cli 0.2.26 → 0.2.28
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 +5 -3
- package/dist/cli.js +15 -1
- package/dist/lib/aggregate-dashboard.js +3 -3
- package/dist/lib/sync.js +9 -1
- package/dist/lib/time.js +17 -0
- package/dist/lib/weekly-score.js +10 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
|
|
23
23
|
个人和聚合面板均提供「周评分」:默认 `50 × √(周消息数 / 200) + 50 × √(周累计额度百分点 / 70)`,消息和额度均合计 Claude + Codex。两项达到基准得 100 分,分数不封顶。
|
|
24
24
|
|
|
25
|
-
个人和团队面板提供「出勤天数」选择,默认 5 天,可选
|
|
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
28
|
|
|
29
|
-
评分按当前查询范围逐周计算,未满一周不折算;查看本周评分请使用 `ccus dashboard serve --range this-week`。聚合面板在「多人对比」中显示周评分,多周取逐周评分的平均值。额度缺失显示 `--`(消息占比为 100% 时可单独评分)。`weekly.csv` 末尾包含 `weeklyScore`、`attendanceDays` 两列:评分与看板共用公式,保留最多 1 位小数,不可用时留空;出勤天数为
|
|
29
|
+
评分按当前查询范围逐周计算,未满一周不折算;查看本周评分请使用 `ccus dashboard serve --range this-week`。聚合面板在「多人对比」中显示周评分,多周取逐周评分的平均值。额度缺失显示 `--`(消息占比为 100% 时可单独评分)。`weekly.csv` 末尾包含 `weeklyScore`、`attendanceDays` 两列:评分与看板共用公式,保留最多 1 位小数,不可用时留空;出勤天数为 0.5~7、步进 0.5,看板导出使用各成员当前选择,命令行默认 5 天。bundle、`daily.csv` 和 `detail.csv` 不变。
|
|
30
30
|
|
|
31
31
|
## 安装
|
|
32
32
|
|
|
@@ -260,6 +260,7 @@ ccus sync config --suffix laptop # 给目标文件名加机器
|
|
|
260
260
|
ccus sync config --no-suffix # 移除已配置的机器后缀
|
|
261
261
|
ccus sync config # 不带参数:打印当前配置
|
|
262
262
|
ccus sync # 用已存配置立即同步一次
|
|
263
|
+
ccus sync --week 2026-09-28 # 重新同步该日期所在整周,仅本次生效
|
|
263
264
|
ccus sync install # 注册系统调度器:每周五 18:00 自动同步(Windows 直接创建计划任务)
|
|
264
265
|
ccus sync uninstall # 卸载系统调度器
|
|
265
266
|
ccus sync status # 查看目标目录、周期、上次同步时间、是否到期
|
|
@@ -271,7 +272,8 @@ ccus sync status # 查看目标目录、周期
|
|
|
271
272
|
- **周一归档上一周**:周一同步时会额外把刚结束的上一整周(`last-week`)导出并归档到对应的上一周子目录(周一是第一个能拿到完整上一周数据的日子);同一天内多次同步用 `sync-state.lastArchivedWeek` 去重,不重复归档。
|
|
272
273
|
- **复制语义**:本地 `data-dir/exports` 仍保留一份,目标目录再放一份,本地照旧可 `aggregate` / `dashboard`。导出产物与 `ccus export` 完全一致,目标目录可直接喂给 `ccus aggregate`。
|
|
273
274
|
- **机器后缀**:`ccus sync config --suffix laptop` 会在**目标目录副本**的文件名扩展名前加 `-laptop`(如 `…_to_2026-06-07-laptop.json.gz`),本地原文件名不变。同一个人多台电脑同步到同一目标目录时,靠后缀让各机器的文件互不覆盖、都保留下来,正好供 `ccus aggregate` 按 personKey 合并去重。用 `ccus sync config --no-suffix` 移除后缀。
|
|
274
|
-
-
|
|
275
|
+
- **补同步指定周**:`ccus sync --week YYYY-MM-DD` 接受周内任一天,按本地时区和已配置的统计日开始时间重新导出周一至周日的数据,再复制到已有目标目录。保留机器后缀,覆盖该周同名文件;不修改配置或自动同步状态,不额外归档上一周。不能与 `--target` / `--interval` / `--range` / `--suffix` / `--no-suffix` 混用。
|
|
276
|
+
- **配置与同步分离**:`ccus sync config` 只读写配置(`--target` / `--interval` / `--range` 写进数据目录下的 `sync-config.json`,可手编;不带参数则打印当前配置);`ccus sync` 按已存配置执行一次同步,直接传入配置参数时仍会先保存再同步。普通同步的上次时间记录在 `sync-state.json`。
|
|
275
277
|
- **周期**:默认 `3h`(每 3 小时最多同步一次的滚动周期);也支持 `daily`(按**自然日**判断——同一天内不重复同步,跨到下一天才再同步)与 `<N>h` / `<N>m` 的滚动周期。
|
|
276
278
|
|
|
277
279
|
### 两种触发方式
|
package/dist/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ const version_1 = require("./lib/version");
|
|
|
72
72
|
/** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
|
|
73
73
|
function printHelp() {
|
|
74
74
|
process.stdout.write("统计日配置:ccus config [--day-start 07:00] [--data-dir PATH]\n统计命令可用 --day-start HH:mm 临时覆盖,默认 07:00(本地时区)。\n\n");
|
|
75
|
-
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus sessions [RANGE] [--out FILE] [--data-dir PATH] (把 Claude 与 Codex 的活跃 session 打包成 zip,默认本周)\n ccus sessions repair [codex|claude] [--min-gap 3h] [--dry-run] (按末条记录时间修复 session 修改时间,默认仅跨天)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
|
|
75
|
+
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus sessions [RANGE] [--out FILE] [--data-dir PATH] (把 Claude 与 Codex 的活跃 session 打包成 zip,默认本周)\n ccus sessions repair [codex|claude] [--min-gap 3h] [--dry-run] (按末条记录时间修复 session 修改时间,默认仅跨天)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--week YYYY-MM-DD] [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
|
|
76
76
|
}
|
|
77
77
|
/** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
|
|
78
78
|
function parseOptions(args) {
|
|
@@ -905,6 +905,18 @@ async function handleBackgroundCheckUpdate(options) {
|
|
|
905
905
|
async function handleSync(options) {
|
|
906
906
|
const startedAt = process.hrtime.bigint();
|
|
907
907
|
const dataDir = getDataDir(options);
|
|
908
|
+
if (options.week !== undefined) {
|
|
909
|
+
const week = getStringOption(options, "week");
|
|
910
|
+
if (!week)
|
|
911
|
+
throw new Error("--week 缺少日期,用法:ccus sync --week YYYY-MM-DD。");
|
|
912
|
+
if (["target", "interval", "range", "suffix", "no-suffix"].some(key => options[key] !== undefined)) {
|
|
913
|
+
throw new Error("--week 不能与同步配置参数同时使用;请先用 ccus sync config 配置目标目录和后缀。");
|
|
914
|
+
}
|
|
915
|
+
const result = await (0, sync_1.performSync)(dataDir, runExport, new Date(), week);
|
|
916
|
+
process.stdout.write(`已重新同步到 ${result.destPath}\n`);
|
|
917
|
+
process.stdout.write(`耗时:${(0, sync_1.formatSyncElapsed)(Number(process.hrtime.bigint() - startedAt) / 1_000_000)}\n`);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
908
920
|
const target = getStringOption(options, "target");
|
|
909
921
|
const interval = getStringOption(options, "interval");
|
|
910
922
|
const range = getStringOption(options, "range");
|
|
@@ -959,6 +971,8 @@ async function handleSyncStatus(options) {
|
|
|
959
971
|
* 不带任何参数时仅打印当前配置,方便确认。
|
|
960
972
|
*/
|
|
961
973
|
async function handleSyncConfig(options) {
|
|
974
|
+
if (options.week !== undefined)
|
|
975
|
+
throw new Error("--week 只用于本次同步,请使用 ccus sync --week YYYY-MM-DD。");
|
|
962
976
|
const dataDir = getDataDir(options);
|
|
963
977
|
const target = getStringOption(options, "target");
|
|
964
978
|
const interval = getStringOption(options, "interval");
|
|
@@ -497,7 +497,7 @@ function weeklyCsvDownloadScript(rows) {
|
|
|
497
497
|
const header = (0, export_1.buildAggregatedWeeklyCsv)([]);
|
|
498
498
|
const data = rows.map(row => ({
|
|
499
499
|
personKey: row.personKey,
|
|
500
|
-
csv:
|
|
500
|
+
csv: weekly_score_1.ATTENDANCE_DAYS.map(days => (0, export_1.buildAggregatedWeeklyCsv)([row], days).slice(header.length + 1)),
|
|
501
501
|
}));
|
|
502
502
|
// 防止用户字段闭合内联脚本;CSV 转义和公式防护复用导出模块。
|
|
503
503
|
const serialized = JSON.stringify({ header, rows: data }).replaceAll("<", "\\u003c");
|
|
@@ -510,9 +510,9 @@ function weeklyCsvDownloadScript(rows) {
|
|
|
510
510
|
const attendance = new Map();
|
|
511
511
|
document.querySelectorAll('[data-attendance-person]').forEach(control => {
|
|
512
512
|
const days = Number(control.value);
|
|
513
|
-
attendance.set(control.dataset.attendancePerson, Number.isInteger(days) && days >=
|
|
513
|
+
attendance.set(control.dataset.attendancePerson, Number.isInteger(days * 2) && days >= 0.5 && days <= 7 ? days : 5);
|
|
514
514
|
});
|
|
515
|
-
const csv = [data.header, ...data.rows.map(row => row.csv[(attendance.get(row.personKey) ?? 5) - 1])].join('\\n');
|
|
515
|
+
const csv = [data.header, ...data.rows.map(row => row.csv[(attendance.get(row.personKey) ?? 5) * 2 - 1])].join('\\n');
|
|
516
516
|
const url = URL.createObjectURL(new Blob(['\\uFEFF', csv], { type: 'text/csv;charset=utf-8' }));
|
|
517
517
|
const link = document.createElement('a');
|
|
518
518
|
link.href = url;
|
package/dist/lib/sync.js
CHANGED
|
@@ -189,11 +189,19 @@ async function exportAndCopy(dataDir, runExport, targetDir, range, suffix) {
|
|
|
189
189
|
* 用 `sync-state.lastArchivedWeek` 去重,避免周一当天每次同步都重复归档。
|
|
190
190
|
* 失败时写 error 状态后向上抛,由调用方决定展示或静默。
|
|
191
191
|
*/
|
|
192
|
-
async function performSync(dataDir, runExport, now = new Date()) {
|
|
192
|
+
async function performSync(dataDir, runExport, now = new Date(), week) {
|
|
193
193
|
const config = readSyncConfig(dataDir);
|
|
194
194
|
if (!config.targetDir) {
|
|
195
195
|
throw new Error("未配置同步目标目录。请先运行 `ccus sync config --target DIR`。");
|
|
196
196
|
}
|
|
197
|
+
// 补同步独立执行,不推迟自动同步,也不触发周一归档。
|
|
198
|
+
if (week !== undefined) {
|
|
199
|
+
const dayStartMinutes = (0, time_1.parseDayStart)((await (0, config_1.readConfig)(dataDir)).dayStart);
|
|
200
|
+
const range = `week:${week}`;
|
|
201
|
+
(0, time_1.resolveRange)(range, now, dayStartMinutes);
|
|
202
|
+
const primary = await exportAndCopy(dataDir, runExport, config.targetDir, range, config.suffix);
|
|
203
|
+
return { ...primary, archivedLastWeekDest: null };
|
|
204
|
+
}
|
|
197
205
|
const state = readSyncStateSync(dataDir);
|
|
198
206
|
let lastArchivedWeek = state?.lastArchivedWeek;
|
|
199
207
|
try {
|
package/dist/lib/time.js
CHANGED
|
@@ -78,6 +78,23 @@ const RANGE_ALIASES = {
|
|
|
78
78
|
function resolveRange(range, now = new Date(), dayStartMinutes = exports.DEFAULT_DAY_START_MINUTES) {
|
|
79
79
|
const raw = (range ?? "5h").trim().toLowerCase();
|
|
80
80
|
const normalized = RANGE_ALIASES[raw] ?? raw;
|
|
81
|
+
if (normalized.startsWith("week:")) {
|
|
82
|
+
const dateKey = normalized.slice(5);
|
|
83
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
|
|
84
|
+
if (!match)
|
|
85
|
+
throw new Error("指定周的日期必须是 YYYY-MM-DD,例如 2026-09-28。");
|
|
86
|
+
const date = new Date(0);
|
|
87
|
+
date.setFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
88
|
+
date.setHours(0, dayStartMinutes, 0, 0);
|
|
89
|
+
if (localDateKey(date) !== dateKey)
|
|
90
|
+
throw new Error(`无效的周日期:${dateKey}。`);
|
|
91
|
+
return {
|
|
92
|
+
label: normalized,
|
|
93
|
+
start: startOfLocalWeek(date, dayStartMinutes),
|
|
94
|
+
end: endOfLocalWeek(date, dayStartMinutes),
|
|
95
|
+
dayStartMinutes,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
81
98
|
if (normalized === "today") {
|
|
82
99
|
return { label: "today", start: startOfReportingDay(now, dayStartMinutes), end: now, dayStartMinutes };
|
|
83
100
|
}
|
package/dist/lib/weekly-score.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
exports.renderAttendanceControl = renderAttendanceControl;
|
|
6
6
|
exports.renderAttendanceScore = renderAttendanceScore;
|
|
@@ -11,9 +11,10 @@ exports.DEFAULT_SCORE_SETTINGS = {
|
|
|
11
11
|
messageBaseline: 200,
|
|
12
12
|
quotaBaseline: 70,
|
|
13
13
|
};
|
|
14
|
+
exports.ATTENDANCE_DAYS = Array.from({ length: 14 }, (_, index) => (index + 1) / 2);
|
|
14
15
|
function calculateWeeklyScore(messages, quota, settings, attendanceDays = 5) {
|
|
15
16
|
const { messageWeight, messageBaseline, quotaBaseline } = settings;
|
|
16
|
-
if (!Number.isInteger(attendanceDays) || attendanceDays <
|
|
17
|
+
if (!Number.isInteger(attendanceDays * 2) || attendanceDays < 0.5 || attendanceDays > 7
|
|
17
18
|
|| !Number.isFinite(messageWeight) || messageWeight < 0 || messageWeight > 100
|
|
18
19
|
|| !Number.isFinite(messageBaseline) || messageBaseline <= 0
|
|
19
20
|
|| !Number.isFinite(quotaBaseline) || quotaBaseline <= 0
|
|
@@ -28,18 +29,18 @@ function calculateWeeklyScore(messages, quota, settings, attendanceDays = 5) {
|
|
|
28
29
|
}
|
|
29
30
|
function renderAttendanceControl(personKey) {
|
|
30
31
|
const attributes = personKey === undefined ? 'id="attendance-days"' : `data-attendance-person="${escapeHtml(personKey)}" aria-label="出勤天数"`;
|
|
31
|
-
const select = `<select ${attributes}>${
|
|
32
|
+
const select = `<select ${attributes}>${exports.ATTENDANCE_DAYS.map(days => `<option value="${days}"${days === 5 ? " selected" : ""}>${days} 天</option>`).join("")}</select>`;
|
|
32
33
|
return personKey === undefined ? `<label>出勤天数 ${select}</label>` : select;
|
|
33
34
|
}
|
|
34
35
|
/** 多周逐周评分后取平均;浏览器切换预先按各出勤天数算好的结果。 */
|
|
35
36
|
function renderAttendanceScore(rows) {
|
|
36
|
-
const scores =
|
|
37
|
+
const scores = exports.ATTENDANCE_DAYS.map(days => {
|
|
37
38
|
const values = rows.map(row => calculateWeeklyScore(row.userMessageCount, row.sevenDayCumulativeUsagePct, exports.DEFAULT_SCORE_SETTINGS, days));
|
|
38
39
|
if (!values.length || values.some(value => value === null))
|
|
39
40
|
return "--";
|
|
40
41
|
return (values.reduce((sum, value) => sum + (value ?? 0), 0) / values.length).toFixed(1);
|
|
41
42
|
});
|
|
42
|
-
return `<span data-attendance-scores="${escapeHtml(JSON.stringify(scores))}">${scores[
|
|
43
|
+
return `<span data-attendance-scores="${escapeHtml(JSON.stringify(scores))}">${scores[9]}</span>`;
|
|
43
44
|
}
|
|
44
45
|
function attendanceScoreScript() {
|
|
45
46
|
return `<script>
|
|
@@ -48,18 +49,18 @@ function attendanceScoreScript() {
|
|
|
48
49
|
if (!select) return;
|
|
49
50
|
select.addEventListener('change', () => {
|
|
50
51
|
const days = Number(select.value);
|
|
51
|
-
if (!Number.isInteger(days) || days <
|
|
52
|
+
if (!Number.isInteger(days * 2) || days < 0.5 || days > 7) return;
|
|
52
53
|
document.querySelectorAll('[data-attendance-scores]').forEach(element => {
|
|
53
|
-
element.textContent = JSON.parse(element.dataset.attendanceScores)[days - 1];
|
|
54
|
+
element.textContent = JSON.parse(element.dataset.attendanceScores)[days * 2 - 1];
|
|
54
55
|
});
|
|
55
56
|
document.querySelectorAll('[data-attendance-person]').forEach(element => { element.value = select.value; });
|
|
56
57
|
});
|
|
57
58
|
document.querySelectorAll('[data-attendance-person]').forEach(control => {
|
|
58
59
|
control.addEventListener('change', () => {
|
|
59
60
|
const days = Number(control.value);
|
|
60
|
-
if (!Number.isInteger(days) || days <
|
|
61
|
+
if (!Number.isInteger(days * 2) || days < 0.5 || days > 7) return;
|
|
61
62
|
const score = control.closest('tr').querySelector('[data-attendance-scores]');
|
|
62
|
-
score.textContent = JSON.parse(score.dataset.attendanceScores)[days - 1];
|
|
63
|
+
score.textContent = JSON.parse(score.dataset.attendanceScores)[days * 2 - 1];
|
|
63
64
|
});
|
|
64
65
|
});
|
|
65
66
|
})();
|