ccus-cli 0.2.13 → 0.2.14
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/dist/cli.js +17 -6
- package/dist/lib/claude.js +77 -98
- package/dist/lib/codex-sessions.js +105 -117
- package/dist/lib/sync.js +5 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -264,8 +264,12 @@ async function loadDashboardData(options, defaultRange) {
|
|
|
264
264
|
// codex 消息柱图走 dailyUserMessages,不受影响。
|
|
265
265
|
const events = (await (0, storage_1.readEventsForRange)(dataDir, range, now))
|
|
266
266
|
.map((record) => (0, payload_1.computeStatuslineEvent)(record));
|
|
267
|
-
const
|
|
268
|
-
|
|
267
|
+
const [claudeUsage, codexUsage] = await Promise.all([
|
|
268
|
+
(0, claude_1.summarizeClaudeProjectUsageCombined)(window.start, window.end),
|
|
269
|
+
(0, codex_sessions_1.summarizeCodexSessionUsageCombined)(window.start, window.end),
|
|
270
|
+
]);
|
|
271
|
+
const claudeDailyUsage = claudeUsage.daily;
|
|
272
|
+
const codexDailyUsage = codexUsage.daily;
|
|
269
273
|
const dailyUserMessages = (0, time_1.enumerateDateKeys)(window.start, window.end).map((date) => ({
|
|
270
274
|
date,
|
|
271
275
|
userMessageCount: claudeDailyUsage.get(date)?.userMessageCount ?? 0,
|
|
@@ -397,10 +401,14 @@ async function runExport(options) {
|
|
|
397
401
|
const statuslineDailyRows = (0, export_1.buildSummaryRows)(claudeEvents);
|
|
398
402
|
const codexStatuslineSummary = (0, dashboard_2.summarizeEvents)(codexEvents);
|
|
399
403
|
const codexStatuslineDailyRows = (0, export_1.buildSummaryRows)(codexEvents);
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
const [claudeScan, codexScan] = await Promise.all([
|
|
405
|
+
(0, claude_1.summarizeClaudeProjectUsageCombined)(window.start, window.end),
|
|
406
|
+
(0, codex_sessions_1.summarizeCodexSessionUsageCombined)(window.start, window.end),
|
|
407
|
+
]);
|
|
408
|
+
const claudeUsage = claudeScan.weekly;
|
|
409
|
+
const claudeDailyUsage = claudeScan.daily;
|
|
410
|
+
const codexUsage = codexScan.weekly;
|
|
411
|
+
const codexDailyUsage = codexScan.daily;
|
|
404
412
|
const claudeWeeklyCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...claudeDailyUsage.values()].map((day) => day.apiEquivalentCost));
|
|
405
413
|
const codexWeeklyCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...codexDailyUsage.values()].map((day) => day.apiEquivalentCost));
|
|
406
414
|
(0, debug_1.debugLog)("export", "data collected", {
|
|
@@ -823,6 +831,7 @@ async function handleBackgroundCheckUpdate(options) {
|
|
|
823
831
|
* 不带参数则用已存配置同步。最终仍无目标目录则报错引导用户先配置。
|
|
824
832
|
*/
|
|
825
833
|
async function handleSync(options) {
|
|
834
|
+
const startedAt = process.hrtime.bigint();
|
|
826
835
|
const dataDir = getDataDir(options);
|
|
827
836
|
const target = getStringOption(options, "target");
|
|
828
837
|
const interval = getStringOption(options, "interval");
|
|
@@ -848,6 +857,8 @@ async function handleSync(options) {
|
|
|
848
857
|
if (result.archivedLastWeekDest) {
|
|
849
858
|
process.stdout.write(`已归档上一周到 ${result.archivedLastWeekDest}\n`);
|
|
850
859
|
}
|
|
860
|
+
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
861
|
+
process.stdout.write(`耗时:${(0, sync_1.formatSyncElapsed)(elapsedMs)}\n`);
|
|
851
862
|
}
|
|
852
863
|
/**
|
|
853
864
|
* `ccus sync status`:打印当前同步配置、上次同步时间与是否到期。
|
package/dist/lib/claude.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.summarizeClaudeProjectUsage = summarizeClaudeProjectUsage;
|
|
7
|
+
exports.summarizeClaudeProjectUsageCombined = summarizeClaudeProjectUsageCombined;
|
|
7
8
|
exports.findActiveSessionFiles = findActiveSessionFiles;
|
|
8
9
|
exports.summarizeClaudeProjectUsageByDay = summarizeClaudeProjectUsageByDay;
|
|
9
10
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
@@ -84,6 +85,14 @@ function localDateKey(date) {
|
|
|
84
85
|
const day = String(date.getDate()).padStart(2, "0");
|
|
85
86
|
return `${year}-${month}-${day}`;
|
|
86
87
|
}
|
|
88
|
+
async function fileMayContainRange(filePath, start) {
|
|
89
|
+
try {
|
|
90
|
+
return (await promises_1.default.stat(filePath)).mtimeMs >= start.getTime();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
87
96
|
/**
|
|
88
97
|
* 判断一条 `type:"user"` 事件是否算作一次用户请求。
|
|
89
98
|
*
|
|
@@ -113,50 +122,18 @@ function isHumanUserMessage(record) {
|
|
|
113
122
|
}
|
|
114
123
|
return true;
|
|
115
124
|
}
|
|
116
|
-
function summarizeProjectTranscript(content, start, end) {
|
|
117
|
-
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
118
|
-
const summary = {
|
|
119
|
-
userMessageCount: 0,
|
|
120
|
-
apiRequestCount: 0,
|
|
121
|
-
inputTokens: 0,
|
|
122
|
-
outputTokens: 0,
|
|
123
|
-
cacheReadInputTokens: 0,
|
|
124
|
-
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
125
|
-
};
|
|
126
|
-
for (const line of lines) {
|
|
127
|
-
try {
|
|
128
|
-
const record = JSON.parse(line);
|
|
129
|
-
const timestamp = isRecord(record) ? getString(record.timestamp) : null;
|
|
130
|
-
if (!isRecord(record) || !timestampInRange(timestamp, start, end)) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (isHumanUserMessage(record)) {
|
|
134
|
-
summary.userMessageCount += 1;
|
|
135
|
-
}
|
|
136
|
-
if (record.type !== "assistant" || !isRecord(record.message) || !isRecord(record.message.usage)) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
const assistantUsage = summarizeAssistantUsage(record.message, timestamp);
|
|
140
|
-
summary.apiRequestCount += 1;
|
|
141
|
-
summary.inputTokens += assistantUsage.inputTokens;
|
|
142
|
-
summary.outputTokens += assistantUsage.outputTokens;
|
|
143
|
-
summary.cacheReadInputTokens += assistantUsage.cacheReadInputTokens;
|
|
144
|
-
summary.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([summary.apiEquivalentCost, assistantUsage.apiEquivalentCost]);
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return summary;
|
|
151
|
-
}
|
|
152
125
|
/**
|
|
153
126
|
* 从 Claude 本地 project transcript 中统计本周消息数、请求数、token 用量和标准 API 等效成本。
|
|
154
127
|
*/
|
|
155
128
|
async function summarizeClaudeProjectUsage(start, end) {
|
|
129
|
+
return (await summarizeClaudeProjectUsageCombined(start, end)).weekly;
|
|
130
|
+
}
|
|
131
|
+
/** 单次扫描同时生成周汇总与按天汇总。 */
|
|
132
|
+
async function summarizeClaudeProjectUsageCombined(start, end) {
|
|
156
133
|
const claudeDataDir = (0, paths_1.getClaudeDataDir)();
|
|
157
134
|
const projectDir = node_path_1.default.join(claudeDataDir, "projects");
|
|
158
135
|
const files = await collectProjectJsonlFiles(projectDir);
|
|
159
|
-
const
|
|
136
|
+
const weekly = {
|
|
160
137
|
userMessageCount: 0,
|
|
161
138
|
apiRequestCount: 0,
|
|
162
139
|
inputTokens: 0,
|
|
@@ -166,22 +143,75 @@ async function summarizeClaudeProjectUsage(start, end) {
|
|
|
166
143
|
matchedFileCount: files.length,
|
|
167
144
|
claudeDataDir,
|
|
168
145
|
};
|
|
146
|
+
const daily = new Map();
|
|
169
147
|
for (const filePath of files) {
|
|
148
|
+
if (!(await fileMayContainRange(filePath, start))) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
let content = "";
|
|
170
152
|
try {
|
|
171
|
-
|
|
172
|
-
const summary = summarizeProjectTranscript(content, start, end);
|
|
173
|
-
totals.userMessageCount += summary.userMessageCount;
|
|
174
|
-
totals.apiRequestCount += summary.apiRequestCount;
|
|
175
|
-
totals.inputTokens += summary.inputTokens;
|
|
176
|
-
totals.outputTokens += summary.outputTokens;
|
|
177
|
-
totals.cacheReadInputTokens += summary.cacheReadInputTokens;
|
|
178
|
-
totals.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([totals.apiEquivalentCost, summary.apiEquivalentCost]);
|
|
153
|
+
content = await promises_1.default.readFile(filePath, "utf8");
|
|
179
154
|
}
|
|
180
155
|
catch {
|
|
181
156
|
continue;
|
|
182
157
|
}
|
|
158
|
+
const fileSummary = {
|
|
159
|
+
userMessageCount: 0,
|
|
160
|
+
apiRequestCount: 0,
|
|
161
|
+
inputTokens: 0,
|
|
162
|
+
outputTokens: 0,
|
|
163
|
+
cacheReadInputTokens: 0,
|
|
164
|
+
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
165
|
+
};
|
|
166
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
try {
|
|
169
|
+
const record = JSON.parse(line);
|
|
170
|
+
const timestamp = isRecord(record) ? getString(record.timestamp) : null;
|
|
171
|
+
if (!isRecord(record) || !timestampInRange(timestamp, start, end)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const date = localDateKey(new Date(timestamp));
|
|
175
|
+
const day = daily.get(date) ?? {
|
|
176
|
+
date,
|
|
177
|
+
userMessageCount: 0,
|
|
178
|
+
apiRequestCount: 0,
|
|
179
|
+
inputTokens: 0,
|
|
180
|
+
outputTokens: 0,
|
|
181
|
+
cacheReadInputTokens: 0,
|
|
182
|
+
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
183
|
+
};
|
|
184
|
+
if (isHumanUserMessage(record)) {
|
|
185
|
+
fileSummary.userMessageCount += 1;
|
|
186
|
+
day.userMessageCount += 1;
|
|
187
|
+
}
|
|
188
|
+
if (record.type === "assistant" && isRecord(record.message) && isRecord(record.message.usage)) {
|
|
189
|
+
const usage = summarizeAssistantUsage(record.message, timestamp);
|
|
190
|
+
fileSummary.apiRequestCount += 1;
|
|
191
|
+
fileSummary.inputTokens += usage.inputTokens;
|
|
192
|
+
fileSummary.outputTokens += usage.outputTokens;
|
|
193
|
+
fileSummary.cacheReadInputTokens += usage.cacheReadInputTokens;
|
|
194
|
+
fileSummary.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([fileSummary.apiEquivalentCost, usage.apiEquivalentCost]);
|
|
195
|
+
day.apiRequestCount += 1;
|
|
196
|
+
day.inputTokens += usage.inputTokens;
|
|
197
|
+
day.outputTokens += usage.outputTokens;
|
|
198
|
+
day.cacheReadInputTokens += usage.cacheReadInputTokens;
|
|
199
|
+
day.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([day.apiEquivalentCost, usage.apiEquivalentCost]);
|
|
200
|
+
}
|
|
201
|
+
daily.set(date, day);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
weekly.userMessageCount += fileSummary.userMessageCount;
|
|
208
|
+
weekly.apiRequestCount += fileSummary.apiRequestCount;
|
|
209
|
+
weekly.inputTokens += fileSummary.inputTokens;
|
|
210
|
+
weekly.outputTokens += fileSummary.outputTokens;
|
|
211
|
+
weekly.cacheReadInputTokens += fileSummary.cacheReadInputTokens;
|
|
212
|
+
weekly.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([weekly.apiEquivalentCost, fileSummary.apiEquivalentCost]);
|
|
183
213
|
}
|
|
184
|
-
return
|
|
214
|
+
return { weekly, daily };
|
|
185
215
|
}
|
|
186
216
|
/**
|
|
187
217
|
* 找出 ~/.claude/projects 中在指定时间范围内有活动的 session 文件,返回文件路径及结构信息。
|
|
@@ -230,57 +260,6 @@ async function findActiveSessionFiles(start, end) {
|
|
|
230
260
|
* 按天汇总 Claude project transcript 中的消息数、请求数、token 用量和标准 API 等效成本。
|
|
231
261
|
*/
|
|
232
262
|
async function summarizeClaudeProjectUsageByDay(start, end) {
|
|
233
|
-
|
|
234
|
-
const projectDir = node_path_1.default.join(claudeDataDir, "projects");
|
|
235
|
-
const files = await collectProjectJsonlFiles(projectDir);
|
|
236
|
-
const daily = new Map();
|
|
237
|
-
for (const filePath of files) {
|
|
238
|
-
let content = "";
|
|
239
|
-
try {
|
|
240
|
-
content = await promises_1.default.readFile(filePath, "utf8");
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
246
|
-
for (const line of lines) {
|
|
247
|
-
try {
|
|
248
|
-
const record = JSON.parse(line);
|
|
249
|
-
if (!isRecord(record)) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const timestamp = getString(record.timestamp);
|
|
253
|
-
if (!timestampInRange(timestamp, start, end)) {
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
const date = localDateKey(new Date(timestamp));
|
|
257
|
-
const current = daily.get(date) ?? {
|
|
258
|
-
date,
|
|
259
|
-
userMessageCount: 0,
|
|
260
|
-
apiRequestCount: 0,
|
|
261
|
-
inputTokens: 0,
|
|
262
|
-
outputTokens: 0,
|
|
263
|
-
cacheReadInputTokens: 0,
|
|
264
|
-
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
265
|
-
};
|
|
266
|
-
if (isHumanUserMessage(record)) {
|
|
267
|
-
current.userMessageCount += 1;
|
|
268
|
-
}
|
|
269
|
-
if (record.type === "assistant" && isRecord(record.message) && isRecord(record.message.usage)) {
|
|
270
|
-
const assistantUsage = summarizeAssistantUsage(record.message, timestamp);
|
|
271
|
-
current.apiRequestCount += 1;
|
|
272
|
-
current.inputTokens += assistantUsage.inputTokens;
|
|
273
|
-
current.outputTokens += assistantUsage.outputTokens;
|
|
274
|
-
current.cacheReadInputTokens += assistantUsage.cacheReadInputTokens;
|
|
275
|
-
current.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([current.apiEquivalentCost, assistantUsage.apiEquivalentCost]);
|
|
276
|
-
}
|
|
277
|
-
daily.set(date, current);
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return daily;
|
|
263
|
+
return (await summarizeClaudeProjectUsageCombined(start, end)).daily;
|
|
285
264
|
}
|
|
286
265
|
//# sourceMappingURL=claude.js.map
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.findActiveCodexSessionFiles = findActiveCodexSessionFiles;
|
|
7
|
+
exports.summarizeCodexSessionUsageCombined = summarizeCodexSessionUsageCombined;
|
|
7
8
|
exports.summarizeCodexSessionUsage = summarizeCodexSessionUsage;
|
|
8
9
|
exports.summarizeCodexSessionUsageByDay = summarizeCodexSessionUsageByDay;
|
|
9
10
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
@@ -126,8 +127,36 @@ function mergeRolloutContents(contents) {
|
|
|
126
127
|
}
|
|
127
128
|
return mergedLines.length > 0 ? `${mergedLines.join("\n")}\n` : "";
|
|
128
129
|
}
|
|
129
|
-
async function
|
|
130
|
-
|
|
130
|
+
async function readRolloutFileRef(filePath) {
|
|
131
|
+
try {
|
|
132
|
+
const stat = await promises_1.default.stat(filePath, { bigint: true });
|
|
133
|
+
return {
|
|
134
|
+
filePath,
|
|
135
|
+
identityKey: stat.ino === 0n ? null : `${stat.dev}:${stat.ino}`,
|
|
136
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { filePath, identityKey: null, mtimeMs: null };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function isCompletePrefix(shorter, longer) {
|
|
144
|
+
return shorter.length === 0
|
|
145
|
+
|| ((shorter.endsWith("\n") || shorter.endsWith("\r")) && longer.startsWith(shorter));
|
|
146
|
+
}
|
|
147
|
+
async function readMergedRollout(group, start) {
|
|
148
|
+
const refs = await Promise.all(group.filePaths.map((filePath) => readRolloutFileRef(filePath)));
|
|
149
|
+
if (start && refs.length > 0 && refs.every((ref) => ref.mtimeMs !== null && ref.mtimeMs < start.getTime())) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const uniqueRefs = new Map();
|
|
153
|
+
for (const ref of refs) {
|
|
154
|
+
const key = ref.identityKey ?? `path:${rolloutPathKey(ref.filePath)}`;
|
|
155
|
+
if (!uniqueRefs.has(key)) {
|
|
156
|
+
uniqueRefs.set(key, ref);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const reads = await Promise.all([...uniqueRefs.values()].map(async ({ filePath }) => {
|
|
131
160
|
try {
|
|
132
161
|
return await promises_1.default.readFile(filePath, "utf8");
|
|
133
162
|
}
|
|
@@ -135,7 +164,18 @@ async function readMergedRollout(group) {
|
|
|
135
164
|
return null;
|
|
136
165
|
}
|
|
137
166
|
}));
|
|
138
|
-
|
|
167
|
+
const contents = reads.filter((content) => content !== null);
|
|
168
|
+
if (contents.length === 0) {
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
if (contents.length === 1 || contents.every((content) => content === contents[0])) {
|
|
172
|
+
return contents[0];
|
|
173
|
+
}
|
|
174
|
+
const longest = contents.reduce((current, content) => content.length > current.length ? content : current);
|
|
175
|
+
if (contents.every((content) => content === longest || isCompletePrefix(content, longest))) {
|
|
176
|
+
return longest;
|
|
177
|
+
}
|
|
178
|
+
return mergeRolloutContents(contents);
|
|
139
179
|
}
|
|
140
180
|
function timestampInRange(timestamp, start, end) {
|
|
141
181
|
if (!timestamp) {
|
|
@@ -155,6 +195,9 @@ async function findActiveCodexSessionFiles(start, end) {
|
|
|
155
195
|
for (const group of groups) {
|
|
156
196
|
try {
|
|
157
197
|
const content = await readMergedRollout(group);
|
|
198
|
+
if (content === null) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
158
201
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
159
202
|
let hasInRange = false;
|
|
160
203
|
for (const line of lines) {
|
|
@@ -203,7 +246,23 @@ function extractLastTokenUsage(payload) {
|
|
|
203
246
|
* turn_id 全局去重(取最早 timestamp),不能在单文件层计数。
|
|
204
247
|
* - `event_msg` + `payload.type=="token_count"`:一次模型请求,token 取 `info.last_token_usage` 增量。
|
|
205
248
|
*/
|
|
206
|
-
function
|
|
249
|
+
function ensureCodexDay(daily, date) {
|
|
250
|
+
let current = daily.get(date);
|
|
251
|
+
if (!current) {
|
|
252
|
+
current = {
|
|
253
|
+
date,
|
|
254
|
+
userMessageCount: 0,
|
|
255
|
+
apiRequestCount: 0,
|
|
256
|
+
inputTokens: 0,
|
|
257
|
+
outputTokens: 0,
|
|
258
|
+
cacheReadInputTokens: 0,
|
|
259
|
+
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
260
|
+
};
|
|
261
|
+
daily.set(date, current);
|
|
262
|
+
}
|
|
263
|
+
return current;
|
|
264
|
+
}
|
|
265
|
+
function summarizeRollout(content, start, end, daily) {
|
|
207
266
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
208
267
|
const result = {
|
|
209
268
|
turns: [],
|
|
@@ -242,21 +301,27 @@ function summarizeRollout(content, start, end) {
|
|
|
242
301
|
if (usage) {
|
|
243
302
|
const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
|
|
244
303
|
const outputTokens = getNumber(usage.output_tokens);
|
|
304
|
+
const cost = (0, api_equivalent_cost_1.priceApiRequest)({
|
|
305
|
+
provider: "codex",
|
|
306
|
+
timestamp: timestamp,
|
|
307
|
+
model: currentModel,
|
|
308
|
+
inputTokens,
|
|
309
|
+
outputTokens,
|
|
310
|
+
cacheReadInputTokens,
|
|
311
|
+
});
|
|
245
312
|
result.apiRequestCount += 1;
|
|
246
313
|
result.inputTokens += inputTokens;
|
|
247
314
|
result.outputTokens += outputTokens;
|
|
248
315
|
result.cacheReadInputTokens += cacheReadInputTokens;
|
|
249
|
-
result.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([
|
|
250
|
-
|
|
251
|
-
(0,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}),
|
|
259
|
-
]);
|
|
316
|
+
result.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([result.apiEquivalentCost, cost]);
|
|
317
|
+
if (daily) {
|
|
318
|
+
const day = ensureCodexDay(daily, (0, time_1.localDateKey)(new Date(timestamp)));
|
|
319
|
+
day.apiRequestCount += 1;
|
|
320
|
+
day.inputTokens += inputTokens;
|
|
321
|
+
day.outputTokens += outputTokens;
|
|
322
|
+
day.cacheReadInputTokens += cacheReadInputTokens;
|
|
323
|
+
day.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([day.apiEquivalentCost, cost]);
|
|
324
|
+
}
|
|
260
325
|
}
|
|
261
326
|
}
|
|
262
327
|
}
|
|
@@ -273,10 +338,11 @@ function summarizeRollout(content, start, end) {
|
|
|
273
338
|
* 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
|
|
274
339
|
* 故用全局 Map<turn_id, minMs> 收集(取最早 timestamp = 真实发生时刻,早于任何重放副本),最后取 size。
|
|
275
340
|
*/
|
|
276
|
-
async function
|
|
341
|
+
async function summarizeCodexSessionUsageCombined(start, end) {
|
|
277
342
|
const codexDataDir = (0, paths_1.getCodexHome)();
|
|
278
343
|
const groups = await collectRolloutFileGroups();
|
|
279
344
|
const turnMinMs = new Map();
|
|
345
|
+
const daily = new Map();
|
|
280
346
|
let apiRequestCount = 0;
|
|
281
347
|
let inputTokens = 0;
|
|
282
348
|
let outputTokens = 0;
|
|
@@ -284,14 +350,14 @@ async function summarizeCodexSessionUsage(start, end) {
|
|
|
284
350
|
let apiEquivalentCost = (0, api_equivalent_cost_1.emptyApiEquivalentCost)();
|
|
285
351
|
for (const group of groups) {
|
|
286
352
|
try {
|
|
287
|
-
const content = await readMergedRollout(group);
|
|
288
|
-
if (isGuardianRollout(content)) {
|
|
353
|
+
const content = await readMergedRollout(group, start);
|
|
354
|
+
if (content === null || isGuardianRollout(content)) {
|
|
289
355
|
continue;
|
|
290
356
|
}
|
|
291
|
-
const parsed = summarizeRollout(content, start, end);
|
|
357
|
+
const parsed = summarizeRollout(content, start, end, daily);
|
|
292
358
|
for (const turn of parsed.turns) {
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
359
|
+
const previous = turnMinMs.get(turn.turnId);
|
|
360
|
+
if (previous === undefined || turn.ms < previous) {
|
|
295
361
|
turnMinMs.set(turn.turnId, turn.ms);
|
|
296
362
|
}
|
|
297
363
|
}
|
|
@@ -305,17 +371,26 @@ async function summarizeCodexSessionUsage(start, end) {
|
|
|
305
371
|
continue;
|
|
306
372
|
}
|
|
307
373
|
}
|
|
374
|
+
for (const ms of turnMinMs.values()) {
|
|
375
|
+
ensureCodexDay(daily, (0, time_1.localDateKey)(new Date(ms))).userMessageCount += 1;
|
|
376
|
+
}
|
|
308
377
|
return {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
378
|
+
weekly: {
|
|
379
|
+
userMessageCount: turnMinMs.size,
|
|
380
|
+
apiRequestCount,
|
|
381
|
+
inputTokens,
|
|
382
|
+
outputTokens,
|
|
383
|
+
cacheReadInputTokens,
|
|
384
|
+
apiEquivalentCost,
|
|
385
|
+
matchedFileCount: groups.length,
|
|
386
|
+
codexDataDir,
|
|
387
|
+
},
|
|
388
|
+
daily,
|
|
317
389
|
};
|
|
318
390
|
}
|
|
391
|
+
async function summarizeCodexSessionUsage(start, end) {
|
|
392
|
+
return (await summarizeCodexSessionUsageCombined(start, end)).weekly;
|
|
393
|
+
}
|
|
319
394
|
/**
|
|
320
395
|
* 按天汇总 Codex session rollout 中的消息数、请求数、token 用量和标准 API 等效成本。
|
|
321
396
|
* 与周汇总一致,排除 Codex Desktop 的 guardian 安全审查 rollout。
|
|
@@ -324,93 +399,6 @@ async function summarizeCodexSessionUsage(start, end) {
|
|
|
324
399
|
* 且重放副本跨天不重复)。token 维度按 token_count 事件 timestamp 的本地日累加。
|
|
325
400
|
*/
|
|
326
401
|
async function summarizeCodexSessionUsageByDay(start, end) {
|
|
327
|
-
|
|
328
|
-
const turnMinMs = new Map();
|
|
329
|
-
const daily = new Map();
|
|
330
|
-
const ensureDay = (date) => {
|
|
331
|
-
let current = daily.get(date);
|
|
332
|
-
if (!current) {
|
|
333
|
-
current = {
|
|
334
|
-
date,
|
|
335
|
-
userMessageCount: 0,
|
|
336
|
-
apiRequestCount: 0,
|
|
337
|
-
inputTokens: 0,
|
|
338
|
-
outputTokens: 0,
|
|
339
|
-
cacheReadInputTokens: 0,
|
|
340
|
-
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
341
|
-
};
|
|
342
|
-
daily.set(date, current);
|
|
343
|
-
}
|
|
344
|
-
return current;
|
|
345
|
-
};
|
|
346
|
-
for (const group of groups) {
|
|
347
|
-
const content = await readMergedRollout(group);
|
|
348
|
-
if (isGuardianRollout(content)) {
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
352
|
-
let currentModel = null;
|
|
353
|
-
for (const line of lines) {
|
|
354
|
-
try {
|
|
355
|
-
const record = JSON.parse(line);
|
|
356
|
-
if (!isRecord(record) || !isRecord(record.payload)) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
const payload = record.payload;
|
|
360
|
-
if (record.type === "turn_context") {
|
|
361
|
-
if (typeof payload.model === "string") {
|
|
362
|
-
currentModel = payload.model;
|
|
363
|
-
}
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
const timestamp = getString(record.timestamp);
|
|
367
|
-
if (!timestampInRange(timestamp, start, end)) {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
if (record.type !== "event_msg") {
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
const ms = new Date(timestamp).getTime();
|
|
374
|
-
if (payload.type === "task_started" && typeof payload.turn_id === "string") {
|
|
375
|
-
if (Number.isFinite(ms)) {
|
|
376
|
-
const prev = turnMinMs.get(payload.turn_id);
|
|
377
|
-
if (prev === undefined || ms < prev) {
|
|
378
|
-
turnMinMs.set(payload.turn_id, ms);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (payload.type === "token_count") {
|
|
383
|
-
const usage = extractLastTokenUsage(payload);
|
|
384
|
-
if (usage) {
|
|
385
|
-
const current = ensureDay((0, time_1.localDateKey)(new Date(ms)));
|
|
386
|
-
const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
|
|
387
|
-
const outputTokens = getNumber(usage.output_tokens);
|
|
388
|
-
current.apiRequestCount += 1;
|
|
389
|
-
current.inputTokens += inputTokens;
|
|
390
|
-
current.outputTokens += outputTokens;
|
|
391
|
-
current.cacheReadInputTokens += cacheReadInputTokens;
|
|
392
|
-
current.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([
|
|
393
|
-
current.apiEquivalentCost,
|
|
394
|
-
(0, api_equivalent_cost_1.priceApiRequest)({
|
|
395
|
-
provider: "codex",
|
|
396
|
-
timestamp: timestamp,
|
|
397
|
-
model: currentModel,
|
|
398
|
-
inputTokens,
|
|
399
|
-
outputTokens,
|
|
400
|
-
cacheReadInputTokens,
|
|
401
|
-
}),
|
|
402
|
-
]);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
for (const ms of turnMinMs.values()) {
|
|
412
|
-
ensureDay((0, time_1.localDateKey)(new Date(ms))).userMessageCount += 1;
|
|
413
|
-
}
|
|
414
|
-
return daily;
|
|
402
|
+
return (await summarizeCodexSessionUsageCombined(start, end)).daily;
|
|
415
403
|
}
|
|
416
404
|
//# sourceMappingURL=codex-sessions.js.map
|
package/dist/lib/sync.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.readSyncConfig = readSyncConfig;
|
|
|
10
10
|
exports.writeSyncConfig = writeSyncConfig;
|
|
11
11
|
exports.readSyncStateSync = readSyncStateSync;
|
|
12
12
|
exports.isSyncDue = isSyncDue;
|
|
13
|
+
exports.formatSyncElapsed = formatSyncElapsed;
|
|
13
14
|
exports.performSync = performSync;
|
|
14
15
|
exports.maybeSpawnBackgroundSync = maybeSpawnBackgroundSync;
|
|
15
16
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -159,6 +160,10 @@ function isSyncDue(config, state, now = new Date()) {
|
|
|
159
160
|
}
|
|
160
161
|
return now.getTime() - last >= interval.ms;
|
|
161
162
|
}
|
|
163
|
+
/** 把同步耗时统一格式化为秒,保留三位小数。 */
|
|
164
|
+
function formatSyncElapsed(elapsedMs) {
|
|
165
|
+
return `${(Math.max(0, elapsedMs) / 1000).toFixed(3)}s`;
|
|
166
|
+
}
|
|
162
167
|
/**
|
|
163
168
|
* 导出某个 range 的 bundle,并复制到目标目录下对应的「按周」子目录。
|
|
164
169
|
*
|