aihezu 2.8.9 → 2.8.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.
Files changed (2) hide show
  1. package/commands/usage.js +36 -158
  2. package/package.json +1 -1
package/commands/usage.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
- const { postJson, getJson } = require('../lib/http');
4
+ const { getJson } = require('../lib/http');
5
5
  const platform = require('../lib/platform');
6
6
 
7
7
  function normalizeHttpUrl(url) {
@@ -295,32 +295,49 @@ function usageHint(current, limit) {
295
295
  return '';
296
296
  }
297
297
 
298
- function getCodexUsageBase(baseUrl) {
299
- // 用户的 Codex base_url 形如 https://xxx.aihezu.dev/openai 或 https://xxx.aihezu.dev
300
- // usage 接口位于 /v1/usage,需要去掉可能存在的 /openai 后缀
298
+ function getUsageApiBase(baseUrl) {
299
+ // 去掉路径中的 /openai 或 /api 后缀
300
+ // - Codex base_url 常见格式 https://xxx/openai
301
+ // - 部分用户 base_url 为 https://xxx/api
302
+ // - Claude 默认 base_url 不带后缀,等价于 no-op
303
+ const stripSuffix = (pathname) => {
304
+ let p = pathname.replace(/\/+$/, '');
305
+ const lower = p.toLowerCase();
306
+ if (lower.endsWith('/openai')) {
307
+ p = p.slice(0, -'/openai'.length);
308
+ } else if (lower.endsWith('/api')) {
309
+ p = p.slice(0, -'/api'.length);
310
+ }
311
+ return p;
312
+ };
313
+
301
314
  try {
302
315
  const url = new URL(baseUrl);
303
- let pathname = url.pathname.replace(/\/+$/, '');
304
- if (pathname.toLowerCase().endsWith('/openai')) {
305
- pathname = pathname.slice(0, -'/openai'.length);
306
- }
316
+ const pathname = stripSuffix(url.pathname);
307
317
  return `${url.origin}${pathname}`;
308
318
  } catch (error) {
309
- return baseUrl.replace(/\/+$/, '').replace(/\/openai$/i, '');
319
+ return stripSuffix(baseUrl.replace(/\/+$/, ''));
310
320
  }
311
321
  }
312
322
 
313
- async function queryCodexUsage(baseUrl, authToken) {
323
+ async function queryUnifiedUsage(baseUrl, authToken) {
314
324
  try {
315
- const apiBase = getCodexUsageBase(baseUrl);
325
+ const apiBase = getUsageApiBase(baseUrl);
316
326
  const usageUrl = `${apiBase}/v1/usage`;
317
327
  const res = await getJson(usageUrl, {
318
328
  headers: { Authorization: `Bearer ${authToken}` }
319
329
  });
320
330
 
321
331
  if (res.statusCode < 200 || res.statusCode >= 300) {
322
- const msg = res.json && (res.json.message || res.json.error || res.json.code);
323
- return { error: msg ? `HTTP ${res.statusCode} - ${msg}` : `HTTP ${res.statusCode}` };
332
+ let detail = '';
333
+ const j = res.json;
334
+ if (j) {
335
+ if (typeof j.message === 'string') detail = j.message;
336
+ else if (typeof j.error === 'string') detail = j.error;
337
+ else if (j.error && typeof j.error.message === 'string') detail = j.error.message;
338
+ else if (typeof j.code === 'string') detail = j.code;
339
+ }
340
+ return { error: detail ? `HTTP ${res.statusCode} - ${detail}` : `HTTP ${res.statusCode}` };
324
341
  }
325
342
 
326
343
  if (!res.json || typeof res.json !== 'object') {
@@ -333,7 +350,7 @@ async function queryCodexUsage(baseUrl, authToken) {
333
350
  }
334
351
  }
335
352
 
336
- function displayCodexUsageStats(stats, origin, source) {
353
+ function displayUnifiedUsageStats(stats, origin, source) {
337
354
  const subscription = stats.subscription || {};
338
355
  const today = (stats.usage && stats.usage.today) || {};
339
356
  const total = (stats.usage && stats.usage.total) || {};
@@ -456,141 +473,6 @@ function displayCodexUsageStats(stats, origin, source) {
456
473
  console.log('');
457
474
  }
458
475
 
459
- async function queryUsage(baseUrl, authToken) {
460
- try {
461
- const origin = new URL(baseUrl).origin;
462
- const keyIdUrl = `${origin}/apiStats/api/get-key-id`;
463
- const statsUrl = `${origin}/apiStats/api/user-stats`;
464
-
465
- const keyIdRes = await postJson(keyIdUrl, { apiKey: authToken });
466
- if (keyIdRes.statusCode < 200 || keyIdRes.statusCode >= 300) {
467
- return { error: `HTTP ${keyIdRes.statusCode}` };
468
- }
469
-
470
- const apiId = keyIdRes?.json?.data?.id || keyIdRes?.json?.id || null;
471
- if (!apiId) {
472
- return { error: '未找到 API Key ID' };
473
- }
474
-
475
- const statsRes = await postJson(statsUrl, { apiId });
476
- if (statsRes.statusCode < 200 || statsRes.statusCode >= 300) {
477
- return { error: `HTTP ${statsRes.statusCode}` };
478
- }
479
-
480
- if (!statsRes.json || typeof statsRes.json !== 'object') {
481
- return { error: '返回内容不是合法的 JSON' };
482
- }
483
-
484
- const stats = (statsRes?.json?.data) ? statsRes.json.data : (statsRes.json || {});
485
- return { stats, origin };
486
- } catch (error) {
487
- return { error: error.message };
488
- }
489
- }
490
-
491
- function displayUsageStats(stats, origin, source) {
492
- const limits = (stats && typeof stats === 'object' && stats.limits && typeof stats.limits === 'object')
493
- ? stats.limits
494
- : {};
495
-
496
- const currentDailyCost = (stats.currentDailyCost !== undefined) ? stats.currentDailyCost : limits.currentDailyCost;
497
- const dailyCostLimit = (stats.dailyCostLimit !== undefined) ? stats.dailyCostLimit : limits.dailyCostLimit;
498
- const currentWindowCost = (stats.currentWindowCost !== undefined) ? stats.currentWindowCost : limits.currentWindowCost;
499
- const rateLimitCost = (stats.rateLimitCost !== undefined) ? stats.rateLimitCost : limits.rateLimitCost;
500
- const windowStartTime = (stats.windowStartTime !== undefined) ? stats.windowStartTime : limits.windowStartTime;
501
- const windowEndTime = (stats.windowEndTime !== undefined) ? stats.windowEndTime : limits.windowEndTime;
502
- const windowRemainingSeconds = (() => {
503
- const raw = (stats.windowRemainingSeconds !== undefined)
504
- ? stats.windowRemainingSeconds
505
- : limits.windowRemainingSeconds;
506
- const provided = asNumber(raw);
507
- if (provided !== null) return provided;
508
- const endMs = normalizeEpochMs(windowEndTime);
509
- if (endMs === null) return null;
510
- return Math.max(0, Math.floor((endMs - Date.now()) / 1000));
511
- })();
512
- const windowDurationMinutes = (() => {
513
- const startMs = normalizeEpochMs(windowStartTime);
514
- const endMs = normalizeEpochMs(windowEndTime);
515
- if (startMs !== null && endMs !== null && endMs > startMs) {
516
- return Math.round((endMs - startMs) / 60000);
517
- }
518
- const raw = (stats.rateLimitWindow !== undefined) ? stats.rateLimitWindow : limits.rateLimitWindow;
519
- const minutes = asNumber(raw);
520
- if (minutes !== null) return minutes;
521
- return null;
522
- })();
523
-
524
- const dailyRemaining = (() => {
525
- const c = asNumber(currentDailyCost);
526
- const l = asNumber(dailyCostLimit);
527
- if (c === null || l === null) return null;
528
- return l - c;
529
- })();
530
-
531
- const windowRemaining = (() => {
532
- const c = asNumber(currentWindowCost);
533
- const l = asNumber(rateLimitCost);
534
- if (c === null || l === null) return null;
535
- return l - c;
536
- })();
537
-
538
- // 提取日请求次数和 Token 用量(API 中 currentWindowRequests/Tokens 实际为日数据)
539
- const currentDailyRequests = (stats.currentDailyRequests !== undefined) ? stats.currentDailyRequests
540
- : (limits.currentDailyRequests !== undefined) ? limits.currentDailyRequests
541
- : (stats.currentWindowRequests !== undefined) ? stats.currentWindowRequests : limits.currentWindowRequests;
542
- const currentDailyTokens = (stats.currentDailyTokens !== undefined) ? stats.currentDailyTokens
543
- : (limits.currentDailyTokens !== undefined) ? limits.currentDailyTokens
544
- : (stats.currentWindowTokens !== undefined) ? stats.currentWindowTokens : limits.currentWindowTokens;
545
-
546
- console.log(`域名: ${origin}`);
547
- console.log(`来源: ${source}`);
548
- console.log('');
549
- console.log(
550
- `日额度: ${formatCost(currentDailyCost)} / ${formatCost(dailyCostLimit)} (${formatPercent(currentDailyCost, dailyCostLimit)}) ` +
551
- `${renderBar(currentDailyCost, dailyCostLimit)} ${usageHint(currentDailyCost, dailyCostLimit)}`.trimEnd()
552
- );
553
- if (dailyRemaining !== null) {
554
- console.log(`日剩余: ${formatCost(dailyRemaining)}`);
555
- }
556
- if (asNumber(currentDailyRequests) !== null || asNumber(currentDailyTokens) !== null) {
557
- const parts = [];
558
- if (asNumber(currentDailyRequests) !== null) parts.push(`请求 ${formatNumber(currentDailyRequests)} 次`);
559
- if (asNumber(currentDailyTokens) !== null) parts.push(`Token ${formatCompactNumber(currentDailyTokens)}`);
560
- console.log(`日统计: ${parts.join(' | ')}`);
561
- }
562
-
563
- console.log('');
564
- console.log(
565
- `窗口: ${formatCost(currentWindowCost)} / ${formatCost(rateLimitCost)} (${formatPercent(currentWindowCost, rateLimitCost)}) ` +
566
- `${renderBar(currentWindowCost, rateLimitCost)} ${usageHint(currentWindowCost, rateLimitCost)}`.trimEnd()
567
- );
568
- if (windowRemaining !== null) {
569
- console.log(`窗口剩余: ${formatCost(windowRemaining)}`);
570
- }
571
- const windowStartText = formatDateTime(windowStartTime);
572
- const windowEndText = formatDateTime(windowEndTime);
573
- if (windowStartText !== '-' || windowEndText !== '-') {
574
- if (windowStartText !== '-' && windowEndText !== '-') {
575
- console.log(`时间窗口: ${windowStartText} ~ ${windowEndText}`);
576
- } else if (windowStartText !== '-') {
577
- console.log(`时间窗口开始: ${windowStartText}`);
578
- } else {
579
- console.log(`时间窗口结束: ${windowEndText}`);
580
- }
581
- }
582
- const windowDurationText = formatDurationMinutes(windowDurationMinutes);
583
- if (windowDurationText !== '-') {
584
- console.log(`周时间窗口: ${windowDurationText}`);
585
- }
586
- const resetCountdownText = formatDurationSeconds(windowRemainingSeconds);
587
- if (resetCountdownText !== '-') {
588
- console.log(`窗口剩余时间: ${resetCountdownText} (距重置)`);
589
- }
590
-
591
- console.log('');
592
- }
593
-
594
476
  function showUsageHelp() {
595
477
  console.log('用法: aihezu usage [service] [--json] [--key <apiKey>]');
596
478
  console.log('');
@@ -688,10 +570,10 @@ async function usageCommand(args = []) {
688
570
  const effectiveKey = overrideKey || config.authToken;
689
571
  const sourceLabel = overrideKey ? `命令行 Key (${config.source})` : config.source;
690
572
  console.log(`[查询中] ${sourceLabel} - Token: ${maskToken(effectiveKey)}`);
691
- const promise = service.name === 'Codex'
692
- ? queryCodexUsage(config.baseUrl, effectiveKey)
693
- : queryUsage(config.baseUrl, effectiveKey);
694
- return { sourceLabel, promise };
573
+ return {
574
+ sourceLabel,
575
+ promise: queryUnifiedUsage(config.baseUrl, effectiveKey)
576
+ };
695
577
  });
696
578
 
697
579
  const results = await Promise.allSettled(requests.map(item => item.promise));
@@ -712,11 +594,7 @@ async function usageCommand(args = []) {
712
594
  });
713
595
  } else {
714
596
  if (!outputJson) {
715
- if (service.name === 'Codex') {
716
- displayCodexUsageStats(result.stats, result.origin, sourceLabel);
717
- } else {
718
- displayUsageStats(result.stats, result.origin, sourceLabel);
719
- }
597
+ displayUnifiedUsageStats(result.stats, result.origin, sourceLabel);
720
598
  }
721
599
  allResults[service.name].push({
722
600
  source: sourceLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aihezu",
3
- "version": "2.8.9",
3
+ "version": "2.8.11",
4
4
  "description": "AI 开发环境配置工具 - 支持 Claude Code, Codex, Google Gemini 的本地化配置、代理设置与缓存清理",
5
5
  "main": "bin/aihezu.js",
6
6
  "bin": {