@token-dashboard/codex-usage-uploader 0.1.0 → 0.1.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/README.md +10 -18
- package/package.json +2 -1
- package/src/cli.js +34 -31
- package/src/collector.js +59 -71
package/README.md
CHANGED
|
@@ -18,14 +18,6 @@ npx @token-dashboard/codex-usage-uploader init
|
|
|
18
18
|
|
|
19
19
|
安装过程中会自动读取 Codex 登录态(`~/.codex/auth.json`)获取身份信息。如果读取不到邮箱,会在终端交互式提示你手动填写。
|
|
20
20
|
|
|
21
|
-
如需跳过交互提示,可以直接指定邮箱:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npx @token-dashboard/codex-usage-uploader init \
|
|
25
|
-
--email you@company.com \
|
|
26
|
-
--yes
|
|
27
|
-
```
|
|
28
|
-
|
|
29
21
|
安装完成后,`codex-usage-uploader` 命令会被写入 `~/bin/`,后续可直接使用。
|
|
30
22
|
|
|
31
23
|
## 命令一览
|
|
@@ -82,16 +74,16 @@ codex-usage-uploader uninstall
|
|
|
82
74
|
|
|
83
75
|
## 常用选项
|
|
84
76
|
|
|
85
|
-
| 选项
|
|
86
|
-
|
|
|
87
|
-
| `--backend-url <url>`
|
|
88
|
-
| `--email <email>`
|
|
89
|
-
| `--employee-name <name>`
|
|
90
|
-
| `--employee-id <id>`
|
|
91
|
-
| `--interval <seconds>`
|
|
92
|
-
| `--yes`
|
|
93
|
-
| `--lines <n>`
|
|
94
|
-
| `-h, --help`
|
|
77
|
+
| 选项 | 说明 |
|
|
78
|
+
| ------------------------ | ------------------------------------------------------ |
|
|
79
|
+
| `--backend-url <url>` | Dashboard 后端地址(默认 `http://101.126.66.51:8086`) |
|
|
80
|
+
| `--email <email>` | 绑定邮箱 |
|
|
81
|
+
| `--employee-name <name>` | 绑定姓名 |
|
|
82
|
+
| `--employee-id <id>` | 绑定工号 |
|
|
83
|
+
| `--interval <seconds>` | 扫描间隔秒数(默认 30) |
|
|
84
|
+
| `--yes` | 跳过所有交互确认 |
|
|
85
|
+
| `--lines <n>` | `logs` 命令输出行数(默认 100) |
|
|
86
|
+
| `-h, --help` | 显示帮助 |
|
|
95
87
|
|
|
96
88
|
## 工作原理
|
|
97
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@token-dashboard/codex-usage-uploader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Codex 用量上报 CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
+
"dev": "node bin/codex-usage-uploader.js init --backend-url http://localhost:8086",
|
|
15
16
|
"test": "node --no-warnings --test tests/*.test.mjs"
|
|
16
17
|
},
|
|
17
18
|
"engines": {
|
package/src/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { CodexUsageUploader } from './collector.js';
|
|
3
4
|
import { identityIsBound, promptConfirm } from './auth.js';
|
|
4
|
-
import { CLI_NAME, DEFAULT_CONFIG_FILE, PRODUCT_NAME } from './constants.js';
|
|
5
|
+
import { CLI_NAME, DEFAULT_BACKEND_URL, DEFAULT_CONFIG_FILE, PRODUCT_NAME } from './constants.js';
|
|
5
6
|
import { findPackageRoot, installCurrentPackage } from './install.js';
|
|
6
7
|
import { LaunchdServiceManager } from './launchd.js';
|
|
7
8
|
import { formatStatusOutput, mergeRuntimeConfig } from './runtime-config.js';
|
|
@@ -256,44 +257,21 @@ function formatDuration(durationMs) {
|
|
|
256
257
|
function printCatchUpProgress(event) {
|
|
257
258
|
switch (event.phase) {
|
|
258
259
|
case 'start':
|
|
259
|
-
console.log(
|
|
260
|
-
`[catch-up] start files=${event.totalFiles} queued_batches=${
|
|
261
|
-
event.bufferingBatchCount +
|
|
262
|
-
event.pendingBatchCount +
|
|
263
|
-
event.retryingBatchCount
|
|
264
|
-
} queued_events=${event.queuedEvents}`,
|
|
265
|
-
);
|
|
260
|
+
console.log(`[scan] start files=${event.totalFiles}`);
|
|
266
261
|
return;
|
|
267
262
|
case 'file':
|
|
268
263
|
console.log(
|
|
269
|
-
`[
|
|
270
|
-
event.bufferingBatchCount +
|
|
271
|
-
event.pendingBatchCount +
|
|
272
|
-
event.retryingBatchCount
|
|
273
|
-
} queued_events=${event.queuedEvents}`,
|
|
274
|
-
);
|
|
275
|
-
return;
|
|
276
|
-
case 'upload':
|
|
277
|
-
console.log(
|
|
278
|
-
`[catch-up] uploaded_batches=${event.batchesUploaded} last_batch=${event.batchKey} remaining_batches=${
|
|
279
|
-
event.bufferingBatchCount +
|
|
280
|
-
event.pendingBatchCount +
|
|
281
|
-
event.retryingBatchCount
|
|
282
|
-
} remaining_events=${event.queuedEvents}`,
|
|
264
|
+
`[scan] file ${event.filesProcessed}/${event.totalFiles} current=${event.file} events=${event.eventsParsed}`,
|
|
283
265
|
);
|
|
284
266
|
return;
|
|
285
267
|
case 'done':
|
|
286
268
|
console.log(
|
|
287
|
-
`[
|
|
269
|
+
`[scan] complete files=${event.filesProcessed}/${event.totalFiles} events=${event.eventsParsed} pending_batches=${event.pendingBatches} duration=${formatDuration(event.durationMs)}`,
|
|
288
270
|
);
|
|
289
271
|
return;
|
|
290
272
|
case 'error':
|
|
291
273
|
console.error(
|
|
292
|
-
`[
|
|
293
|
-
event.bufferingBatchCount +
|
|
294
|
-
event.pendingBatchCount +
|
|
295
|
-
event.retryingBatchCount
|
|
296
|
-
} remaining_events=${event.queuedEvents}`,
|
|
274
|
+
`[scan] failed stage=${event.stage} message=${event.message}`,
|
|
297
275
|
);
|
|
298
276
|
return;
|
|
299
277
|
}
|
|
@@ -307,8 +285,26 @@ function wrapCatchUpFailure(error) {
|
|
|
307
285
|
);
|
|
308
286
|
}
|
|
309
287
|
|
|
288
|
+
function printPathHint(runtime) {
|
|
289
|
+
const linkDir = path.dirname(runtime.homeBinLink);
|
|
290
|
+
const dirs = (process.env.PATH || '').split(path.delimiter);
|
|
291
|
+
if (dirs.includes(linkDir)) return;
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(
|
|
294
|
+
`Note: ${linkDir} is not in your PATH. To use \`${CLI_NAME}\` directly, run:`,
|
|
295
|
+
);
|
|
296
|
+
console.log(` export PATH="${linkDir}:$PATH"`);
|
|
297
|
+
console.log(
|
|
298
|
+
'You can add this line to ~/.zshrc or ~/.bashrc to make it permanent.',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
310
302
|
async function runInit(options) {
|
|
311
|
-
|
|
303
|
+
const overrides = runtimeOverrides(options);
|
|
304
|
+
if (!overrides.backendUrl) {
|
|
305
|
+
overrides.backendUrl = DEFAULT_BACKEND_URL;
|
|
306
|
+
}
|
|
307
|
+
let runtime = mergeRuntimeConfig(options.configFile, overrides);
|
|
312
308
|
if (!runtime.backendUrl) {
|
|
313
309
|
throw new Error(
|
|
314
310
|
'--backend-url is required for init unless already configured in config.json',
|
|
@@ -332,10 +328,11 @@ async function runInit(options) {
|
|
|
332
328
|
console.log(`Install root: ${runtime.installRoot}`);
|
|
333
329
|
console.log(`Config file: ${runtime.configFile}`);
|
|
334
330
|
printIdentity(identity);
|
|
335
|
-
console.log('
|
|
331
|
+
console.log('Scanning local Codex sessions...');
|
|
336
332
|
|
|
333
|
+
let scanResult;
|
|
337
334
|
try {
|
|
338
|
-
await uploader.runForegroundCatchUp({
|
|
335
|
+
scanResult = await uploader.runForegroundCatchUp({
|
|
339
336
|
onProgress: printCatchUpProgress,
|
|
340
337
|
});
|
|
341
338
|
} catch (error) {
|
|
@@ -344,7 +341,13 @@ async function runInit(options) {
|
|
|
344
341
|
|
|
345
342
|
manager.start();
|
|
346
343
|
console.log(`${PRODUCT_NAME} initialized and started.`);
|
|
344
|
+
if (scanResult.pendingBatches > 0) {
|
|
345
|
+
console.log(
|
|
346
|
+
`Background service is uploading ${scanResult.pendingBatches} batch(es) with ${scanResult.pendingEvents} event(s).`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
347
349
|
console.log(`Use \`${CLI_NAME} status\` to check the local service.`);
|
|
350
|
+
printPathHint(runtime);
|
|
348
351
|
} finally {
|
|
349
352
|
uploader.close();
|
|
350
353
|
}
|
package/src/collector.js
CHANGED
|
@@ -465,7 +465,7 @@ export class CodexUsageUploader {
|
|
|
465
465
|
const response = await fetch(`${this.backendUrl}${apiPath}`, {
|
|
466
466
|
method: 'POST',
|
|
467
467
|
headers: { 'Content-Type': 'application/json' },
|
|
468
|
-
body:
|
|
468
|
+
body: JSON.stringify(payload),
|
|
469
469
|
});
|
|
470
470
|
if (!response.ok) {
|
|
471
471
|
const text = await response.text();
|
|
@@ -489,7 +489,7 @@ export class CodexUsageUploader {
|
|
|
489
489
|
this.stateDb.setCheckpoint('last_register_at', String(nowTs()));
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
async flushPendingBatches({ failFast = false, onBatchUploaded } = {}) {
|
|
492
|
+
async flushPendingBatches({ failFast = false, concurrency = 5, onBatchUploaded } = {}) {
|
|
493
493
|
if (!this.backendUrl) {
|
|
494
494
|
return { uploadedBatches: 0, failedBatches: 0, lastError: null };
|
|
495
495
|
}
|
|
@@ -509,42 +509,70 @@ export class CodexUsageUploader {
|
|
|
509
509
|
let uploadedBatches = 0;
|
|
510
510
|
let failedBatches = 0;
|
|
511
511
|
let lastError = null;
|
|
512
|
+
const collectorBody = this.collectorRequestBody();
|
|
512
513
|
|
|
513
|
-
|
|
514
|
+
const rows = this.stateDb.iterDuePendingBatches();
|
|
515
|
+
|
|
516
|
+
const uploadOne = async (row) => {
|
|
514
517
|
const payload = this.sanitizeUploadPayload(JSON.parse(row.payload_json));
|
|
515
518
|
const requestBody = {
|
|
516
519
|
idempotencyKey: row.batch_key,
|
|
517
|
-
collector:
|
|
520
|
+
collector: collectorBody,
|
|
518
521
|
payloadSizeBytes: Number(row.payload_bytes),
|
|
519
522
|
sessions: payload.sessions ?? [],
|
|
520
523
|
turns: payload.turns ?? [],
|
|
521
524
|
events: payload.events ?? [],
|
|
522
525
|
};
|
|
526
|
+
await this.postJson('/codex-usage/upload', requestBody);
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const pending = new Set();
|
|
530
|
+
let rowIndex = 0;
|
|
531
|
+
|
|
532
|
+
const enqueue = () => {
|
|
533
|
+
while (pending.size < concurrency && rowIndex < rows.length) {
|
|
534
|
+
const row = rows[rowIndex++];
|
|
535
|
+
const task = uploadOne(row)
|
|
536
|
+
.then(() => {
|
|
537
|
+
this.stateDb.markBatchUploaded(row.id);
|
|
538
|
+
uploadedBatches += 1;
|
|
539
|
+
onBatchUploaded?.({ row, uploadedBatches });
|
|
540
|
+
})
|
|
541
|
+
.catch((error) => {
|
|
542
|
+
this.stateDb.markBatchFailed(
|
|
543
|
+
row.id,
|
|
544
|
+
Number(row.attempt_count) + 1,
|
|
545
|
+
error instanceof Error ? error.message : String(error),
|
|
546
|
+
);
|
|
547
|
+
failedBatches += 1;
|
|
548
|
+
lastError = error;
|
|
549
|
+
if (failFast) {
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
.finally(() => {
|
|
554
|
+
pending.delete(task);
|
|
555
|
+
});
|
|
556
|
+
pending.add(task);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
enqueue();
|
|
561
|
+
while (pending.size > 0) {
|
|
523
562
|
try {
|
|
524
|
-
await
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
onBatchUploaded?.({
|
|
528
|
-
row,
|
|
529
|
-
uploadedBatches,
|
|
530
|
-
queueStats: this.getQueueStats(),
|
|
531
|
-
});
|
|
532
|
-
} catch (error) {
|
|
533
|
-
this.stateDb.markBatchFailed(
|
|
534
|
-
row.id,
|
|
535
|
-
Number(row.attempt_count) + 1,
|
|
536
|
-
error instanceof Error ? error.message : String(error),
|
|
537
|
-
);
|
|
538
|
-
failedBatches += 1;
|
|
539
|
-
lastError = error;
|
|
540
|
-
if (failFast) {
|
|
541
|
-
throw new Error(
|
|
542
|
-
`Upload batch ${row.batch_key} failed: ${
|
|
543
|
-
error instanceof Error ? error.message : String(error)
|
|
544
|
-
}`,
|
|
545
|
-
);
|
|
546
|
-
}
|
|
563
|
+
await Promise.race(pending);
|
|
564
|
+
} catch {
|
|
565
|
+
if (failFast) break;
|
|
547
566
|
}
|
|
567
|
+
enqueue();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (failFast && lastError) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`Upload batch failed: ${
|
|
573
|
+
lastError instanceof Error ? lastError.message : String(lastError)
|
|
574
|
+
}`,
|
|
575
|
+
);
|
|
548
576
|
}
|
|
549
577
|
|
|
550
578
|
return { uploadedBatches, failedBatches, lastError };
|
|
@@ -578,15 +606,12 @@ export class CodexUsageUploader {
|
|
|
578
606
|
filesProcessed: 0,
|
|
579
607
|
eventsParsed: 0,
|
|
580
608
|
batchesQueued: 0,
|
|
581
|
-
batchesUploaded: 0,
|
|
582
|
-
...this.getQueueStats(),
|
|
583
609
|
});
|
|
584
610
|
|
|
585
611
|
let scanResult;
|
|
586
612
|
try {
|
|
587
613
|
scanResult = await this.scanSnapshotEntries(backfillSnapshot, {
|
|
588
614
|
onFileProcessed: ({ entry, filesProcessed, totals }) => {
|
|
589
|
-
const queueStats = this.getQueueStats();
|
|
590
615
|
onProgress?.({
|
|
591
616
|
phase: 'file',
|
|
592
617
|
file: entry.progressPath ?? entry.relpath,
|
|
@@ -594,8 +619,6 @@ export class CodexUsageUploader {
|
|
|
594
619
|
filesProcessed,
|
|
595
620
|
eventsParsed: totals.events,
|
|
596
621
|
batchesQueued: totals.batchesQueued,
|
|
597
|
-
batchesUploaded: 0,
|
|
598
|
-
...queueStats,
|
|
599
622
|
});
|
|
600
623
|
},
|
|
601
624
|
});
|
|
@@ -604,44 +627,13 @@ export class CodexUsageUploader {
|
|
|
604
627
|
phase: 'error',
|
|
605
628
|
stage: 'scan',
|
|
606
629
|
message: error instanceof Error ? error.message : String(error),
|
|
607
|
-
...this.getQueueStats(),
|
|
608
630
|
});
|
|
609
631
|
throw error;
|
|
610
632
|
}
|
|
611
633
|
|
|
612
634
|
scanResult.batchesQueued += this.stateDb.sealStaleBatches(true);
|
|
613
|
-
this.stateDb.markAllPendingDue();
|
|
614
635
|
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
flushResult = await this.flushPendingBatches({
|
|
618
|
-
failFast: true,
|
|
619
|
-
onBatchUploaded: ({ row, uploadedBatches, queueStats }) => {
|
|
620
|
-
onProgress?.({
|
|
621
|
-
phase: 'upload',
|
|
622
|
-
batchKey: row.batch_key,
|
|
623
|
-
totalFiles,
|
|
624
|
-
filesProcessed: totalFiles,
|
|
625
|
-
eventsParsed: scanResult.events,
|
|
626
|
-
batchesQueued: scanResult.batchesQueued,
|
|
627
|
-
batchesUploaded: uploadedBatches,
|
|
628
|
-
...queueStats,
|
|
629
|
-
});
|
|
630
|
-
},
|
|
631
|
-
});
|
|
632
|
-
} catch (error) {
|
|
633
|
-
onProgress?.({
|
|
634
|
-
phase: 'error',
|
|
635
|
-
stage: 'upload',
|
|
636
|
-
message: error instanceof Error ? error.message : String(error),
|
|
637
|
-
totalFiles,
|
|
638
|
-
filesProcessed: totalFiles,
|
|
639
|
-
eventsParsed: scanResult.events,
|
|
640
|
-
batchesQueued: scanResult.batchesQueued,
|
|
641
|
-
...this.getQueueStats(),
|
|
642
|
-
});
|
|
643
|
-
throw error;
|
|
644
|
-
}
|
|
636
|
+
await this.ensureRemoteRegistration();
|
|
645
637
|
|
|
646
638
|
const queueStats = this.getQueueStats();
|
|
647
639
|
const result = {
|
|
@@ -651,14 +643,10 @@ export class CodexUsageUploader {
|
|
|
651
643
|
sessionsParsed: scanResult.sessions,
|
|
652
644
|
turnsParsed: scanResult.turns,
|
|
653
645
|
batchesQueued: scanResult.batchesQueued,
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
queueStats.pendingBatchCount +
|
|
658
|
-
queueStats.retryingBatchCount,
|
|
659
|
-
remainingQueuedEvents: queueStats.queuedEvents,
|
|
646
|
+
pendingBatches:
|
|
647
|
+
queueStats.pendingBatchCount + queueStats.retryingBatchCount,
|
|
648
|
+
pendingEvents: queueStats.queuedEvents,
|
|
660
649
|
durationMs: Date.now() - startedAt,
|
|
661
|
-
...queueStats,
|
|
662
650
|
};
|
|
663
651
|
|
|
664
652
|
onProgress?.({
|