ccus-cli 0.2.9 → 0.2.11
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 +8 -5
- package/dist/cli.js +55 -17
- package/dist/lib/aggregate-dashboard.js +67 -8
- package/dist/lib/aggregate.js +113 -23
- package/dist/lib/api-equivalent-cost.js +189 -0
- package/dist/lib/api-pricing-catalog.json +270 -0
- package/dist/lib/api-pricing-table.js +163 -0
- package/dist/lib/claude.js +52 -9
- package/dist/lib/codex-sessions.js +153 -45
- package/dist/lib/dashboard-pages.js +15 -0
- package/dist/lib/dashboard.js +35 -1
- package/dist/lib/export.js +12 -0
- package/dist/lib/paths.js +37 -0
- package/package.json +3 -2
|
@@ -8,6 +8,7 @@ exports.summarizeCodexSessionUsage = summarizeCodexSessionUsage;
|
|
|
8
8
|
exports.summarizeCodexSessionUsageByDay = summarizeCodexSessionUsageByDay;
|
|
9
9
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const api_equivalent_cost_1 = require("./api-equivalent-cost");
|
|
11
12
|
const paths_1 = require("./paths");
|
|
12
13
|
const time_1 = require("./time");
|
|
13
14
|
function isRecord(value) {
|
|
@@ -19,6 +20,13 @@ function getNumber(value) {
|
|
|
19
20
|
function getString(value) {
|
|
20
21
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
21
22
|
}
|
|
23
|
+
function splitInputTokens(usage) {
|
|
24
|
+
const cacheReadInputTokens = getNumber(usage.cached_input_tokens);
|
|
25
|
+
return {
|
|
26
|
+
inputTokens: Math.max(0, getNumber(usage.input_tokens) - cacheReadInputTokens),
|
|
27
|
+
cacheReadInputTokens,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
22
30
|
/** Guardian 是 Codex Desktop 的动作安全审查子代理,不代表用户请求或业务模型调用。 */
|
|
23
31
|
function isGuardianRollout(content) {
|
|
24
32
|
let offset = 0;
|
|
@@ -61,6 +69,74 @@ async function collectRolloutFiles(directoryPath) {
|
|
|
61
69
|
throw error;
|
|
62
70
|
}
|
|
63
71
|
}
|
|
72
|
+
function rolloutPathKey(relativePath) {
|
|
73
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
74
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
75
|
+
}
|
|
76
|
+
/** 收集标准 Codex 与 Orca session,并按 sessions 下的相对路径归并同一 rollout。 */
|
|
77
|
+
async function collectRolloutFileGroups() {
|
|
78
|
+
const sessionDirs = (0, paths_1.getCodexSessionHomes)().map((home) => node_path_1.default.join(home, "sessions"));
|
|
79
|
+
const filesByDir = await Promise.all(sessionDirs.map((directory) => collectRolloutFiles(directory)));
|
|
80
|
+
const groups = new Map();
|
|
81
|
+
for (let index = 0; index < sessionDirs.length; index += 1) {
|
|
82
|
+
const sessionsDir = sessionDirs[index];
|
|
83
|
+
for (const filePath of filesByDir[index]) {
|
|
84
|
+
const relativePath = node_path_1.default.relative(sessionsDir, filePath);
|
|
85
|
+
const key = rolloutPathKey(relativePath);
|
|
86
|
+
const group = groups.get(key);
|
|
87
|
+
if (group) {
|
|
88
|
+
group.filePaths.push(filePath);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
groups.set(key, { relativePath, filePaths: [filePath] });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return [...groups.values()];
|
|
96
|
+
}
|
|
97
|
+
function rolloutLineKey(line) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.stringify(JSON.parse(line));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return line;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 合并同一 rollout 在不同根目录下的副本。
|
|
107
|
+
*
|
|
108
|
+
* 对每种 JSONL 行保留各副本中的最大出现次数,既消除完整/部分镜像造成的重复,
|
|
109
|
+
* 又保留任一副本独有的新增事件以及单个源内真实存在的重复行。
|
|
110
|
+
*/
|
|
111
|
+
function mergeRolloutContents(contents) {
|
|
112
|
+
const mergedLines = [];
|
|
113
|
+
const mergedCounts = new Map();
|
|
114
|
+
for (const content of contents) {
|
|
115
|
+
const sourceCounts = new Map();
|
|
116
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const key = rolloutLineKey(line);
|
|
119
|
+
const sourceCount = (sourceCounts.get(key) ?? 0) + 1;
|
|
120
|
+
sourceCounts.set(key, sourceCount);
|
|
121
|
+
if (sourceCount > (mergedCounts.get(key) ?? 0)) {
|
|
122
|
+
mergedCounts.set(key, sourceCount);
|
|
123
|
+
mergedLines.push(line);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return mergedLines.length > 0 ? `${mergedLines.join("\n")}\n` : "";
|
|
128
|
+
}
|
|
129
|
+
async function readMergedRollout(group) {
|
|
130
|
+
const reads = await Promise.all(group.filePaths.map(async (filePath) => {
|
|
131
|
+
try {
|
|
132
|
+
return await promises_1.default.readFile(filePath, "utf8");
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
return mergeRolloutContents(reads.filter((content) => content !== null));
|
|
139
|
+
}
|
|
64
140
|
function timestampInRange(timestamp, start, end) {
|
|
65
141
|
if (!timestamp) {
|
|
66
142
|
return false;
|
|
@@ -69,17 +145,16 @@ function timestampInRange(timestamp, start, end) {
|
|
|
69
145
|
return Number.isFinite(value) && value >= start.getTime() && value <= end.getTime();
|
|
70
146
|
}
|
|
71
147
|
/**
|
|
72
|
-
*
|
|
148
|
+
* 找出标准 Codex 与 Orca sessions 中在指定时间范围内有活动的 rollout 文件。
|
|
73
149
|
*
|
|
74
|
-
*
|
|
150
|
+
* 只判断合并后的文件里是否存在范围内的记录,不过滤内容,导出时写入完整合并结果。
|
|
75
151
|
*/
|
|
76
152
|
async function findActiveCodexSessionFiles(start, end) {
|
|
77
|
-
const
|
|
78
|
-
const files = await collectRolloutFiles(sessionsDir);
|
|
153
|
+
const groups = await collectRolloutFileGroups();
|
|
79
154
|
const result = [];
|
|
80
|
-
for (const
|
|
155
|
+
for (const group of groups) {
|
|
81
156
|
try {
|
|
82
|
-
const content = await
|
|
157
|
+
const content = await readMergedRollout(group);
|
|
83
158
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
84
159
|
let hasInRange = false;
|
|
85
160
|
for (const line of lines) {
|
|
@@ -96,8 +171,9 @@ async function findActiveCodexSessionFiles(start, end) {
|
|
|
96
171
|
}
|
|
97
172
|
if (hasInRange) {
|
|
98
173
|
result.push({
|
|
99
|
-
filePath,
|
|
100
|
-
relativePath:
|
|
174
|
+
filePath: group.filePaths[0],
|
|
175
|
+
relativePath: group.relativePath,
|
|
176
|
+
content,
|
|
101
177
|
});
|
|
102
178
|
}
|
|
103
179
|
}
|
|
@@ -110,8 +186,7 @@ async function findActiveCodexSessionFiles(start, end) {
|
|
|
110
186
|
/**
|
|
111
187
|
* 从 `token_count` 事件的 `info.last_token_usage` 取 token 用量对象(缺失返回 null)。
|
|
112
188
|
* 必须用 last_token_usage(本次增量),不能用 total_token_usage(会话累计,会重复计)。
|
|
113
|
-
*
|
|
114
|
-
* 累加 inputTokens 时要减去 cached 得到净输入,对齐 Claude 的 `input_tokens` 口径。
|
|
189
|
+
* `cached_input_tokens` 是 `input_tokens` 的子集;普通输入需扣除缓存输入并钳制为非负数。
|
|
115
190
|
*/
|
|
116
191
|
function extractLastTokenUsage(payload) {
|
|
117
192
|
if (!isRecord(payload.info)) {
|
|
@@ -136,19 +211,28 @@ function summarizeRollout(content, start, end) {
|
|
|
136
211
|
inputTokens: 0,
|
|
137
212
|
outputTokens: 0,
|
|
138
213
|
cacheReadInputTokens: 0,
|
|
214
|
+
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
139
215
|
};
|
|
216
|
+
let currentModel = null;
|
|
140
217
|
for (const line of lines) {
|
|
141
218
|
try {
|
|
142
219
|
const record = JSON.parse(line);
|
|
143
|
-
if (!isRecord(record) || !
|
|
220
|
+
if (!isRecord(record) || !isRecord(record.payload)) {
|
|
144
221
|
continue;
|
|
145
222
|
}
|
|
146
|
-
|
|
223
|
+
const payload = record.payload;
|
|
224
|
+
if (record.type === "turn_context") {
|
|
225
|
+
if (typeof payload.model === "string") {
|
|
226
|
+
currentModel = payload.model;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const timestamp = getString(record.timestamp);
|
|
231
|
+
if (!timestampInRange(timestamp, start, end) || record.type !== "event_msg") {
|
|
147
232
|
continue;
|
|
148
233
|
}
|
|
149
|
-
const payload = record.payload;
|
|
150
234
|
if (payload.type === "task_started" && typeof payload.turn_id === "string") {
|
|
151
|
-
const ms = new Date(
|
|
235
|
+
const ms = new Date(timestamp ?? "").getTime();
|
|
152
236
|
if (Number.isFinite(ms)) {
|
|
153
237
|
result.turns.push({ turnId: payload.turn_id, ms });
|
|
154
238
|
}
|
|
@@ -156,12 +240,23 @@ function summarizeRollout(content, start, end) {
|
|
|
156
240
|
if (payload.type === "token_count") {
|
|
157
241
|
const usage = extractLastTokenUsage(payload);
|
|
158
242
|
if (usage) {
|
|
159
|
-
|
|
160
|
-
const
|
|
243
|
+
const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
|
|
244
|
+
const outputTokens = getNumber(usage.output_tokens);
|
|
161
245
|
result.apiRequestCount += 1;
|
|
162
|
-
result.inputTokens +=
|
|
163
|
-
result.outputTokens +=
|
|
164
|
-
result.cacheReadInputTokens +=
|
|
246
|
+
result.inputTokens += inputTokens;
|
|
247
|
+
result.outputTokens += outputTokens;
|
|
248
|
+
result.cacheReadInputTokens += cacheReadInputTokens;
|
|
249
|
+
result.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([
|
|
250
|
+
result.apiEquivalentCost,
|
|
251
|
+
(0, api_equivalent_cost_1.priceApiRequest)({
|
|
252
|
+
provider: "codex",
|
|
253
|
+
timestamp: timestamp,
|
|
254
|
+
model: currentModel,
|
|
255
|
+
inputTokens,
|
|
256
|
+
outputTokens,
|
|
257
|
+
cacheReadInputTokens,
|
|
258
|
+
}),
|
|
259
|
+
]);
|
|
165
260
|
}
|
|
166
261
|
}
|
|
167
262
|
}
|
|
@@ -172,7 +267,7 @@ function summarizeRollout(content, start, end) {
|
|
|
172
267
|
return result;
|
|
173
268
|
}
|
|
174
269
|
/**
|
|
175
|
-
*
|
|
270
|
+
* 从标准 Codex 与 Orca 本地 session rollout 统计消息数、请求数、token 用量和标准 API 等效成本。
|
|
176
271
|
* Codex Desktop 的 guardian 安全审查 rollout 整体排除,避免把内部审批轮次计为用户使用量。
|
|
177
272
|
*
|
|
178
273
|
* 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
|
|
@@ -180,16 +275,16 @@ function summarizeRollout(content, start, end) {
|
|
|
180
275
|
*/
|
|
181
276
|
async function summarizeCodexSessionUsage(start, end) {
|
|
182
277
|
const codexDataDir = (0, paths_1.getCodexHome)();
|
|
183
|
-
const
|
|
184
|
-
const files = await collectRolloutFiles(sessionsDir);
|
|
278
|
+
const groups = await collectRolloutFileGroups();
|
|
185
279
|
const turnMinMs = new Map();
|
|
186
280
|
let apiRequestCount = 0;
|
|
187
281
|
let inputTokens = 0;
|
|
188
282
|
let outputTokens = 0;
|
|
189
283
|
let cacheReadInputTokens = 0;
|
|
190
|
-
|
|
284
|
+
let apiEquivalentCost = (0, api_equivalent_cost_1.emptyApiEquivalentCost)();
|
|
285
|
+
for (const group of groups) {
|
|
191
286
|
try {
|
|
192
|
-
const content = await
|
|
287
|
+
const content = await readMergedRollout(group);
|
|
193
288
|
if (isGuardianRollout(content)) {
|
|
194
289
|
continue;
|
|
195
290
|
}
|
|
@@ -204,6 +299,7 @@ async function summarizeCodexSessionUsage(start, end) {
|
|
|
204
299
|
inputTokens += parsed.inputTokens;
|
|
205
300
|
outputTokens += parsed.outputTokens;
|
|
206
301
|
cacheReadInputTokens += parsed.cacheReadInputTokens;
|
|
302
|
+
apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([apiEquivalentCost, parsed.apiEquivalentCost]);
|
|
207
303
|
}
|
|
208
304
|
catch {
|
|
209
305
|
continue;
|
|
@@ -215,21 +311,20 @@ async function summarizeCodexSessionUsage(start, end) {
|
|
|
215
311
|
inputTokens,
|
|
216
312
|
outputTokens,
|
|
217
313
|
cacheReadInputTokens,
|
|
218
|
-
|
|
314
|
+
apiEquivalentCost,
|
|
315
|
+
matchedFileCount: groups.length,
|
|
219
316
|
codexDataDir,
|
|
220
317
|
};
|
|
221
318
|
}
|
|
222
319
|
/**
|
|
223
|
-
* 按天汇总 Codex session rollout
|
|
320
|
+
* 按天汇总 Codex session rollout 中的消息数、请求数、token 用量和标准 API 等效成本。
|
|
224
321
|
* 与周汇总一致,排除 Codex Desktop 的 guardian 安全审查 rollout。
|
|
225
322
|
*
|
|
226
323
|
* 消息数同 weekly:先全局 Map<turn_id, minMs> 去重,再按 minMs 的本地日归桶(保证 weekly = Σ daily、
|
|
227
324
|
* 且重放副本跨天不重复)。token 维度按 token_count 事件 timestamp 的本地日累加。
|
|
228
325
|
*/
|
|
229
326
|
async function summarizeCodexSessionUsageByDay(start, end) {
|
|
230
|
-
const
|
|
231
|
-
const sessionsDir = node_path_1.default.join(codexDataDir, "sessions");
|
|
232
|
-
const files = await collectRolloutFiles(sessionsDir);
|
|
327
|
+
const groups = await collectRolloutFileGroups();
|
|
233
328
|
const turnMinMs = new Map();
|
|
234
329
|
const daily = new Map();
|
|
235
330
|
const ensureDay = (date) => {
|
|
@@ -242,37 +337,39 @@ async function summarizeCodexSessionUsageByDay(start, end) {
|
|
|
242
337
|
inputTokens: 0,
|
|
243
338
|
outputTokens: 0,
|
|
244
339
|
cacheReadInputTokens: 0,
|
|
340
|
+
apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
|
|
245
341
|
};
|
|
246
342
|
daily.set(date, current);
|
|
247
343
|
}
|
|
248
344
|
return current;
|
|
249
345
|
};
|
|
250
|
-
for (const
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
content = await promises_1.default.readFile(filePath, "utf8");
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
346
|
+
for (const group of groups) {
|
|
347
|
+
const content = await readMergedRollout(group);
|
|
258
348
|
if (isGuardianRollout(content)) {
|
|
259
349
|
continue;
|
|
260
350
|
}
|
|
261
351
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
352
|
+
let currentModel = null;
|
|
262
353
|
for (const line of lines) {
|
|
263
354
|
try {
|
|
264
355
|
const record = JSON.parse(line);
|
|
265
|
-
if (!isRecord(record)) {
|
|
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
|
+
}
|
|
266
364
|
continue;
|
|
267
365
|
}
|
|
268
366
|
const timestamp = getString(record.timestamp);
|
|
269
367
|
if (!timestampInRange(timestamp, start, end)) {
|
|
270
368
|
continue;
|
|
271
369
|
}
|
|
272
|
-
if (record.type !== "event_msg"
|
|
370
|
+
if (record.type !== "event_msg") {
|
|
273
371
|
continue;
|
|
274
372
|
}
|
|
275
|
-
const payload = record.payload;
|
|
276
373
|
const ms = new Date(timestamp).getTime();
|
|
277
374
|
if (payload.type === "task_started" && typeof payload.turn_id === "string") {
|
|
278
375
|
if (Number.isFinite(ms)) {
|
|
@@ -286,12 +383,23 @@ async function summarizeCodexSessionUsageByDay(start, end) {
|
|
|
286
383
|
const usage = extractLastTokenUsage(payload);
|
|
287
384
|
if (usage) {
|
|
288
385
|
const current = ensureDay((0, time_1.localDateKey)(new Date(ms)));
|
|
289
|
-
|
|
290
|
-
const
|
|
386
|
+
const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
|
|
387
|
+
const outputTokens = getNumber(usage.output_tokens);
|
|
291
388
|
current.apiRequestCount += 1;
|
|
292
|
-
current.inputTokens +=
|
|
293
|
-
current.outputTokens +=
|
|
294
|
-
current.cacheReadInputTokens +=
|
|
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
|
+
]);
|
|
295
403
|
}
|
|
296
404
|
}
|
|
297
405
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDashboardPage = renderDashboardPage;
|
|
4
|
+
const api_pricing_table_1 = require("./api-pricing-table");
|
|
5
|
+
/** 两类 dashboard serve 共用的页面路由;null 表示 404。 */
|
|
6
|
+
async function renderDashboardPage(pathname, renderDashboard, generatedAt = new Date()) {
|
|
7
|
+
if (pathname === "/") {
|
|
8
|
+
return renderDashboard();
|
|
9
|
+
}
|
|
10
|
+
if (pathname === api_pricing_table_1.API_PRICING_PAGE_PATH) {
|
|
11
|
+
return (0, api_pricing_table_1.buildApiPricingPage)(generatedAt);
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=dashboard-pages.js.map
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.bucketizeEvents = bucketizeEvents;
|
|
|
5
5
|
exports.buildDashboardHtml = buildDashboardHtml;
|
|
6
6
|
const aggregate_1 = require("./aggregate");
|
|
7
7
|
const chart_assets_1 = require("./chart-assets");
|
|
8
|
+
const api_pricing_table_1 = require("./api-pricing-table");
|
|
8
9
|
const payload_1 = require("./payload");
|
|
9
10
|
const time_1 = require("./time");
|
|
10
11
|
/** 所有插入到 HTML/SVG 的动态文本都先转义,避免本地页面被注入内容。 */
|
|
@@ -216,6 +217,36 @@ function renderDailyMessages(points) {
|
|
|
216
217
|
function statValue(value, suffix = "%") {
|
|
217
218
|
return value === null ? "--" : `${value.toFixed(1)}${suffix}`;
|
|
218
219
|
}
|
|
220
|
+
function formatCostUsd(value) {
|
|
221
|
+
return `$${value < 0.01 ? value.toFixed(4) : value.toFixed(2)}`;
|
|
222
|
+
}
|
|
223
|
+
function costDisplay(result) {
|
|
224
|
+
if (result.pricedApiRequestCount === 0 && result.unpricedApiRequestCount > 0) {
|
|
225
|
+
return { value: "不可用", note: `${result.unpricedApiRequestCount} 个请求未定价` };
|
|
226
|
+
}
|
|
227
|
+
const amount = formatCostUsd(result.estimatedUsd ?? 0);
|
|
228
|
+
if (result.unpricedApiRequestCount > 0) {
|
|
229
|
+
return { value: `≥ ${amount}`, note: `${result.unpricedApiRequestCount} 个请求未定价,结果不完整` };
|
|
230
|
+
}
|
|
231
|
+
return { value: amount, note: `${result.pricedApiRequestCount} 个请求已定价` };
|
|
232
|
+
}
|
|
233
|
+
function renderApiEquivalentCostCard(costs) {
|
|
234
|
+
if (!costs) {
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
const display = costDisplay(costs.total);
|
|
238
|
+
return `<article class="panel stat-card">
|
|
239
|
+
<h2>合计等效 API 成本</h2>
|
|
240
|
+
<p class="stat-value">${escapeHtml(display.value)}</p>
|
|
241
|
+
<p class="stat-note">${escapeHtml(display.note)}</p>
|
|
242
|
+
<a class="pricing-link" href="${api_pricing_table_1.API_PRICING_PAGE_FILE}" target="_blank" rel="noopener noreferrer">查看当前价格表</a>
|
|
243
|
+
</article>`;
|
|
244
|
+
}
|
|
245
|
+
function renderApiEquivalentCostNote(costs, pricingCatalogVersion) {
|
|
246
|
+
return costs
|
|
247
|
+
? `<p class="muted">标准同步 API 等效成本,不是订阅或实际账单 · 事件时间计价 · 目录 ${escapeHtml(pricingCatalogVersion ?? "不可用")}</p>`
|
|
248
|
+
: "";
|
|
249
|
+
}
|
|
219
250
|
/** 最近事件表帮助回看 statusline 在某一时刻到底输出了什么。 */
|
|
220
251
|
function renderRecentEvents(events) {
|
|
221
252
|
const rows = [...events]
|
|
@@ -263,7 +294,7 @@ function renderRecentEvents(events) {
|
|
|
263
294
|
*
|
|
264
295
|
* 整个 dashboard 保持“单文件可打开”,方便导出和直接分享本地结果。
|
|
265
296
|
*/
|
|
266
|
-
function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages = []) {
|
|
297
|
+
function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages = [], apiEquivalentCost = null, pricingCatalogVersion = null) {
|
|
267
298
|
const summary = summarizeEvents(events);
|
|
268
299
|
const buckets = bucketizeEvents(events, start, end, pickBucketMinutes(start, end));
|
|
269
300
|
const totalUserMessages = dailyUserMessages.reduce((sum, point) => sum + point.userMessageCount, 0);
|
|
@@ -373,6 +404,7 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
|
|
|
373
404
|
color: var(--muted);
|
|
374
405
|
font-size: 14px;
|
|
375
406
|
}
|
|
407
|
+
.pricing-link { display: inline-block; margin-top: 12px; color: var(--accent); font-size: 13px; }
|
|
376
408
|
.panel-header {
|
|
377
409
|
display: flex;
|
|
378
410
|
align-items: end;
|
|
@@ -464,7 +496,9 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
|
|
|
464
496
|
<p class="stat-value">${combinedUserMessages}</p>
|
|
465
497
|
<p class="stat-note">窗口内每日真实用户请求数合计(Claude ${totalUserMessages} · Codex ${codexTotalUserMessages})</p>
|
|
466
498
|
</article>
|
|
499
|
+
${renderApiEquivalentCostCard(apiEquivalentCost)}
|
|
467
500
|
</section>
|
|
501
|
+
${renderApiEquivalentCostNote(apiEquivalentCost, pricingCatalogVersion)}
|
|
468
502
|
${renderChart(buckets)}
|
|
469
503
|
${renderDailyMessages(dailyUserMessages)}
|
|
470
504
|
${renderRecentEvents(events)}
|
package/dist/lib/export.js
CHANGED
|
@@ -28,6 +28,10 @@ function quoteCsv(value) {
|
|
|
28
28
|
function toMillions(value) {
|
|
29
29
|
return value === null ? null : (0, time_1.roundNumber)(value / 1_000_000, 6);
|
|
30
30
|
}
|
|
31
|
+
/** 等效 API 成本以美元输出,最多保留 6 位小数;不可用时留空。 */
|
|
32
|
+
function formatApiEquivalentCost(value) {
|
|
33
|
+
return value === null ? null : (0, time_1.roundNumber)(value, 6);
|
|
34
|
+
}
|
|
31
35
|
/**
|
|
32
36
|
* 清理控制字符,并防止 Excel/Sheets 把值解释成公式。
|
|
33
37
|
*
|
|
@@ -219,6 +223,8 @@ function buildAggregatedDailyCsv(rows) {
|
|
|
219
223
|
"sevenDayCumulativeUsagePct",
|
|
220
224
|
"uniqueSessions",
|
|
221
225
|
"uniqueWorkspaces",
|
|
226
|
+
"estimatedApiEquivalentCostUsd",
|
|
227
|
+
"pricingCatalogVersion",
|
|
222
228
|
];
|
|
223
229
|
const lines = rows.map((row) => toCsvLine([
|
|
224
230
|
row.personKey,
|
|
@@ -236,6 +242,8 @@ function buildAggregatedDailyCsv(rows) {
|
|
|
236
242
|
row.sevenDayCumulativeUsagePct,
|
|
237
243
|
row.uniqueSessions,
|
|
238
244
|
row.uniqueWorkspaces,
|
|
245
|
+
formatApiEquivalentCost(row.estimatedApiEquivalentCostUsd),
|
|
246
|
+
row.pricingCatalogVersion,
|
|
239
247
|
]));
|
|
240
248
|
return [header.join(","), ...lines].join("\n");
|
|
241
249
|
}
|
|
@@ -257,6 +265,8 @@ function buildAggregatedWeeklyCsv(rows) {
|
|
|
257
265
|
"sevenDayCumulativeUsagePct",
|
|
258
266
|
"uniqueSessions",
|
|
259
267
|
"uniqueWorkspaces",
|
|
268
|
+
"estimatedApiEquivalentCostUsd",
|
|
269
|
+
"pricingCatalogVersion",
|
|
260
270
|
];
|
|
261
271
|
const lines = rows.map((row) => toCsvLine([
|
|
262
272
|
row.personKey,
|
|
@@ -274,6 +284,8 @@ function buildAggregatedWeeklyCsv(rows) {
|
|
|
274
284
|
row.sevenDayCumulativeUsagePct,
|
|
275
285
|
row.uniqueSessions,
|
|
276
286
|
row.uniqueWorkspaces,
|
|
287
|
+
formatApiEquivalentCost(row.estimatedApiEquivalentCostUsd),
|
|
288
|
+
row.pricingCatalogVersion,
|
|
277
289
|
]));
|
|
278
290
|
return [header.join(","), ...lines].join("\n");
|
|
279
291
|
}
|
package/dist/lib/paths.js
CHANGED
|
@@ -13,6 +13,8 @@ exports.getApiConfigPath = getApiConfigPath;
|
|
|
13
13
|
exports.getApiQuotaCachePath = getApiQuotaCachePath;
|
|
14
14
|
exports.getCodexQuotaCachePath = getCodexQuotaCachePath;
|
|
15
15
|
exports.getCodexHome = getCodexHome;
|
|
16
|
+
exports.getOrcaCodexHome = getOrcaCodexHome;
|
|
17
|
+
exports.getCodexSessionHomes = getCodexSessionHomes;
|
|
16
18
|
exports.getCodexConfigPath = getCodexConfigPath;
|
|
17
19
|
exports.getCodexHooksPath = getCodexHooksPath;
|
|
18
20
|
exports.getClaudeDataDir = getClaudeDataDir;
|
|
@@ -78,6 +80,41 @@ function getCodexQuotaCachePath(dataDir) {
|
|
|
78
80
|
function getCodexHome() {
|
|
79
81
|
return process.env.CODEX_HOME ?? node_path_1.default.join(node_os_1.default.homedir(), ".codex");
|
|
80
82
|
}
|
|
83
|
+
/** Orca 内置 Codex 的运行时数据目录;非 Orca 环境下返回 null。 */
|
|
84
|
+
function getOrcaCodexHome() {
|
|
85
|
+
const appData = process.env.APPDATA;
|
|
86
|
+
return appData ? node_path_1.default.join(appData, "orca", "codex-runtime-home", "home") : null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Codex session 的全部数据源。
|
|
90
|
+
*
|
|
91
|
+
* 默认合并 `~/.codex` 与 Orca runtime home;`CODEX_HOME` 指向 Orca 时仍保留
|
|
92
|
+
* `~/.codex`,其它显式覆盖则沿用原有覆盖语义。路径按平台规则去重。
|
|
93
|
+
*/
|
|
94
|
+
function getCodexSessionHomes() {
|
|
95
|
+
const configuredHome = node_path_1.default.resolve(getCodexHome());
|
|
96
|
+
const defaultHome = node_path_1.default.resolve(node_path_1.default.join(node_os_1.default.homedir(), ".codex"));
|
|
97
|
+
const orcaHome = getOrcaCodexHome();
|
|
98
|
+
const candidates = orcaHome && samePath(configuredHome, orcaHome)
|
|
99
|
+
? [defaultHome, configuredHome]
|
|
100
|
+
: [configuredHome, ...(orcaHome ? [node_path_1.default.resolve(orcaHome)] : [])];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
return candidates.filter((candidate) => {
|
|
103
|
+
const key = pathKey(candidate);
|
|
104
|
+
if (seen.has(key)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
seen.add(key);
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function pathKey(value) {
|
|
112
|
+
const resolved = node_path_1.default.resolve(value);
|
|
113
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
114
|
+
}
|
|
115
|
+
function samePath(left, right) {
|
|
116
|
+
return pathKey(left) === pathKey(right);
|
|
117
|
+
}
|
|
81
118
|
/** Codex CLI 用户级 config.toml 路径,`ccus install --codex` 往里写 notify。 */
|
|
82
119
|
function getCodexConfigPath() {
|
|
83
120
|
return node_path_1.default.join(getCodexHome(), "config.toml");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccus-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Claude Code statusline usage logger and dashboard CLI",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/cli.js",
|
|
11
11
|
"dist/lib/**/*.js",
|
|
12
|
+
"dist/lib/api-pricing-catalog.json",
|
|
12
13
|
"dist/types.js",
|
|
13
14
|
"dist/vendor/**"
|
|
14
15
|
],
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
"build": "tsc -p tsconfig.json && node scripts/copy-vendor.js",
|
|
17
18
|
"prepublishOnly": "npm run build",
|
|
18
19
|
"test": "node --test --test-concurrency 1 \"dist/test/**/*.test.js\"",
|
|
19
|
-
"test:src": "node --import tsx --test --test-concurrency 1 src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts src/test/sync.test.ts src/test/scheduler.test.ts src/test/api-mode.test.ts src/test/codex-fetcher.test.ts src/test/codex-sessions.test.ts src/test/sessions.test.ts src/test/codex-install.test.ts src/test/cli-codex-notify.test.ts src/test/cli-codex-hook.test.ts src/test/cli-install-codex.test.ts",
|
|
20
|
+
"test:src": "node --import tsx --test --test-concurrency 1 src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/api-equivalent-cost.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts src/test/sync.test.ts src/test/scheduler.test.ts src/test/api-mode.test.ts src/test/codex-fetcher.test.ts src/test/codex-sessions.test.ts src/test/sessions.test.ts src/test/codex-install.test.ts src/test/cli-codex-notify.test.ts src/test/cli-codex-hook.test.ts src/test/cli-install-codex.test.ts",
|
|
20
21
|
"smoke:api": "node scripts/smoke-api.mjs"
|
|
21
22
|
},
|
|
22
23
|
"keywords": [
|