aihezu 2.8.12 → 2.8.13

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 +132 -133
  2. package/package.json +1 -1
package/commands/usage.js CHANGED
@@ -231,32 +231,16 @@ function formatDateTime(value) {
231
231
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
232
232
  }
233
233
 
234
- function formatDurationSeconds(value) {
235
- const total = asNumber(value);
236
- if (total === null) return '-';
237
- let remaining = Math.max(0, Math.floor(total));
238
- const days = Math.floor(remaining / 86400);
239
- remaining %= 86400;
240
- const hours = Math.floor(remaining / 3600);
241
- remaining %= 3600;
242
- const minutes = Math.floor(remaining / 60);
243
- const seconds = remaining % 60;
244
-
245
- const parts = [];
246
- if (days) parts.push(`${days} 天`);
247
- if (hours) parts.push(`${hours} 小时`);
248
- if (minutes) parts.push(`${minutes} 分`);
249
- if (seconds || parts.length === 0) parts.push(`${seconds} 秒`);
250
- return parts.join(' ');
251
- }
252
-
253
- function formatDurationMinutes(value) {
254
- const minutes = asNumber(value);
255
- if (minutes === null || minutes <= 0) return '-';
256
- const human = formatDurationSeconds(minutes * 60);
257
- if (human === '-') return '-';
258
- const minuteText = Number.isInteger(minutes) ? String(minutes) : minutes.toFixed(2);
259
- return `${human} (${minuteText} 分钟)`;
234
+ // 把毫秒格式化为简洁的人类可读时长:11661.24 → "11.7s",950 → "950ms",65000 → "1m5s"
235
+ function formatDuration(ms) {
236
+ const n = asNumber(ms);
237
+ if (n === null || n < 0) return '-';
238
+ if (n < 1000) return `${Math.round(n)}ms`;
239
+ const seconds = n / 1000;
240
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
241
+ const m = Math.floor(seconds / 60);
242
+ const s = Math.round(seconds % 60);
243
+ return s ? `${m}m${s}s` : `${m}m`;
260
244
  }
261
245
 
262
246
  function formatNumber(value) {
@@ -305,7 +289,12 @@ async function queryUnifiedUsage(baseUrl, authToken) {
305
289
  const apiBase = getUsageApiBase(baseUrl);
306
290
  const usageUrl = `${apiBase}/v1/usage`;
307
291
  const res = await getJson(usageUrl, {
308
- headers: { Authorization: `Bearer ${authToken}` }
292
+ headers: {
293
+ Authorization: `Bearer ${authToken}`,
294
+ // 防御性禁用任何中间缓存,确保每次拿到最新快照
295
+ 'Cache-Control': 'no-cache',
296
+ Pragma: 'no-cache'
297
+ }
309
298
  });
310
299
 
311
300
  if (res.statusCode < 200 || res.statusCode >= 300) {
@@ -330,123 +319,130 @@ async function queryUnifiedUsage(baseUrl, authToken) {
330
319
  }
331
320
  }
332
321
 
333
- function displayUnifiedUsageStats(stats, origin, source) {
334
- const subscription = stats.subscription || {};
335
- const today = (stats.usage && stats.usage.today) || {};
336
- const total = (stats.usage && stats.usage.total) || {};
322
+ // 渲染订阅制额度行(仅当账号返回了 *_limit_usd 字段时使用)
323
+ function renderQuotaLine(label, used, limit) {
324
+ const u = asNumber(used);
325
+ const l = asNumber(limit);
326
+ if (l === null || l <= 0) return null;
327
+ return `${label}: ${formatCost(u)} / ${formatCost(l)} (${formatPercent(u, l)}) ` +
328
+ `${renderBar(u, l)} ${usageHint(u, l)}`.trimEnd();
329
+ }
337
330
 
338
- const dailyUsage = asNumber(subscription.daily_usage_usd);
339
- const dailyLimit = asNumber(subscription.daily_limit_usd);
340
- const weeklyUsage = asNumber(subscription.weekly_usage_usd);
341
- const weeklyLimit = asNumber(subscription.weekly_limit_usd);
342
- const monthlyUsage = asNumber(subscription.monthly_usage_usd);
343
- const monthlyLimit = asNumber(subscription.monthly_limit_usd);
331
+ // 余额 / 额度区块:钱包模式显示余额,订阅模式显示日/周/月额度进度条
332
+ function displayBalanceOrQuota(stats) {
333
+ const sub = stats.subscription || {};
334
+ const lines = [];
335
+
336
+ const dailyLine = renderQuotaLine('日额度', sub.daily_usage_usd, sub.daily_limit_usd);
337
+ const weeklyLine = renderQuotaLine('周额度', sub.weekly_usage_usd, sub.weekly_limit_usd);
338
+ const monthlyLine = renderQuotaLine('月额度', sub.monthly_usage_usd, sub.monthly_limit_usd);
339
+ if (dailyLine) lines.push(dailyLine);
340
+ if (weeklyLine) lines.push(weeklyLine);
341
+ if (monthlyLine) lines.push(monthlyLine);
342
+
343
+ if (sub.expires_at) {
344
+ const expiresMs = Date.parse(sub.expires_at);
345
+ if (!Number.isNaN(expiresMs)) lines.push(`套餐到期: ${formatDateTime(expiresMs)}`);
346
+ }
344
347
 
345
- const unit = stats.unit || 'USD';
346
- const planName = stats.planName || '-';
347
- const mode = stats.mode || '-';
348
- const isValid = stats.isValid === false ? '否' : '是';
348
+ // 钱包余额(无订阅额度时)
349
+ const balance = asNumber(stats.remaining) !== null ? asNumber(stats.remaining) : asNumber(stats.balance);
350
+ if (!lines.length && balance !== null) {
351
+ lines.push(`余额: ${formatCost(balance)}`);
352
+ }
349
353
 
350
- console.log(`域名: ${origin}`);
351
- console.log(`来源: ${source}`);
352
- console.log(`套餐: ${planName} | 模式: ${mode} | 有效: ${isValid} | 单位: ${unit}`);
353
- console.log('');
354
+ // 累计已用(usage.total.cost 优先,回退到 model_stats 合计)
355
+ const totalCost = asNumber(stats.usage && stats.usage.total && stats.usage.total.cost);
356
+ if (totalCost !== null) lines.push(`累计已用: ${formatCost(totalCost)}`);
354
357
 
355
- if (dailyUsage !== null || dailyLimit !== null) {
356
- const usageText = formatCost(dailyUsage);
357
- if (dailyLimit !== null && dailyLimit > 0) {
358
- console.log(
359
- `日额度: ${usageText} / ${formatCost(dailyLimit)} (${formatPercent(dailyUsage, dailyLimit)}) ` +
360
- `${renderBar(dailyUsage, dailyLimit)} ${usageHint(dailyUsage, dailyLimit)}`.trimEnd()
361
- );
362
- const dailyRemaining = asNumber(stats.remaining);
363
- if (dailyRemaining !== null) {
364
- console.log(`日剩余: ${formatCost(dailyRemaining)}`);
365
- } else if (dailyUsage !== null) {
366
- console.log(`日剩余: ${formatCost(dailyLimit - dailyUsage)}`);
367
- }
368
- } else {
369
- console.log(`日用量: ${usageText} (无限制)`);
370
- }
358
+ if (lines.length) {
359
+ console.log('');
360
+ lines.forEach(line => console.log(line));
371
361
  }
362
+ }
372
363
 
373
- if (weeklyUsage !== null && weeklyLimit !== null && weeklyLimit > 0) {
374
- console.log(
375
- `周额度: ${formatCost(weeklyUsage)} / ${formatCost(weeklyLimit)} (${formatPercent(weeklyUsage, weeklyLimit)}) ` +
376
- `${renderBar(weeklyUsage, weeklyLimit)} ${usageHint(weeklyUsage, weeklyLimit)}`.trimEnd()
377
- );
378
- } else if (weeklyUsage !== null && weeklyUsage > 0) {
379
- console.log(`周用量: ${formatCost(weeklyUsage)}`);
380
- }
364
+ function displayUnifiedUsageStats(stats, origin, source, tokenMask) {
365
+ const usage = stats.usage || {};
366
+ const today = usage.today || {};
367
+ const total = usage.total || {};
381
368
 
382
- if (monthlyUsage !== null && monthlyLimit !== null && monthlyLimit > 0) {
383
- console.log(
384
- `月额度: ${formatCost(monthlyUsage)} / ${formatCost(monthlyLimit)} (${formatPercent(monthlyUsage, monthlyLimit)}) ` +
385
- `${renderBar(monthlyUsage, monthlyLimit)} ${usageHint(monthlyUsage, monthlyLimit)}`.trimEnd()
386
- );
387
- } else if (monthlyUsage !== null && monthlyUsage > 0) {
388
- console.log(`月用量: ${formatCost(monthlyUsage)}`);
389
- }
369
+ const unit = stats.unit || 'USD';
370
+ const planName = stats.planName || '-';
371
+ const mode = stats.mode || '-';
372
+ const valid = stats.isValid === false ? '失效' : '有效';
373
+ const host = origin.replace(/^https?:\/\//, '');
390
374
 
391
- const expiresAt = subscription.expires_at;
392
- if (expiresAt) {
393
- const expiresMs = Date.parse(expiresAt);
394
- if (!Number.isNaN(expiresMs)) {
395
- console.log(`套餐到期: ${formatDateTime(expiresMs)}`);
396
- }
397
- }
375
+ console.log(`域名: ${host}`);
376
+ console.log(`来源: ${source}${tokenMask ? ' · ' + tokenMask : ''}`);
377
+ console.log(`套餐: ${planName} | ${mode} | ${valid} | ${unit}`);
398
378
 
399
- if (Object.keys(today).length > 0) {
379
+ // 余额 / 订阅额度
380
+ displayBalanceOrQuota(stats);
381
+
382
+ // 今日(日期取 daily_usage 末项,回退 today 自带)
383
+ const daily = Array.isArray(stats.daily_usage) ? stats.daily_usage : [];
384
+ const todayDate = (daily.length && daily[daily.length - 1].date) || today.date || '';
385
+ if (Object.keys(today).length) {
400
386
  console.log('');
401
- console.log('今日:');
402
- const todayParts = [];
403
- if (asNumber(today.requests) !== null) todayParts.push(`请求 ${formatNumber(today.requests)} 次`);
404
- if (asNumber(today.total_tokens) !== null) todayParts.push(`Token ${formatCompactNumber(today.total_tokens)}`);
405
- if (asNumber(today.input_tokens) !== null) todayParts.push(`输入 ${formatCompactNumber(today.input_tokens)}`);
406
- if (asNumber(today.output_tokens) !== null) todayParts.push(`输出 ${formatCompactNumber(today.output_tokens)}`);
407
- if (asNumber(today.cache_read_tokens) !== null) todayParts.push(`缓存读 ${formatCompactNumber(today.cache_read_tokens)}`);
408
- if (asNumber(today.cache_creation_tokens) !== null) todayParts.push(`缓存写 ${formatCompactNumber(today.cache_creation_tokens)}`);
409
- if (todayParts.length) console.log(` ${todayParts.join(' | ')}`);
410
- if (asNumber(today.cost) !== null) console.log(` 费用: ${formatCost(today.cost)}`);
387
+ console.log(`[今日${todayDate ? ' ' + todayDate : ''}]`);
388
+ const head = [];
389
+ if (asNumber(today.requests) !== null) head.push(`请求 ${formatNumber(today.requests)} 次`);
390
+ if (asNumber(today.total_tokens) !== null) head.push(`Token ${formatCompactNumber(today.total_tokens)}`);
391
+ if (asNumber(today.cost) !== null) head.push(`费用 ${formatCost(today.cost)}`);
392
+ if (head.length) console.log(` ${head.join(' | ')}`);
393
+
394
+ const detail = [];
395
+ if (asNumber(today.input_tokens) !== null) detail.push(`输入 ${formatCompactNumber(today.input_tokens)}`);
396
+ if (asNumber(today.output_tokens) !== null) detail.push(`输出 ${formatCompactNumber(today.output_tokens)}`);
397
+ const cacheRead = asNumber(today.cache_read_tokens);
398
+ const cacheWrite = asNumber(today.cache_creation_tokens) !== null
399
+ ? asNumber(today.cache_creation_tokens) : asNumber(today.cache_write_tokens);
400
+ if (cacheRead !== null) detail.push(`缓存读 ${formatCompactNumber(cacheRead)}`);
401
+ if (cacheWrite !== null) detail.push(`缓存写 ${formatCompactNumber(cacheWrite)}`);
402
+ if (detail.length) console.log(` ${detail.join(' | ')}`);
411
403
  }
412
404
 
413
- if (Object.keys(total).length > 0 && total !== today) {
414
- const totalRequests = asNumber(total.requests);
415
- const totalCost = asNumber(total.cost);
416
- if (totalRequests !== null || totalCost !== null) {
417
- console.log('');
418
- console.log('累计:');
419
- const parts = [];
420
- if (totalRequests !== null) parts.push(`请求 ${formatNumber(total.requests)} 次`);
421
- if (asNumber(total.total_tokens) !== null) parts.push(`Token ${formatCompactNumber(total.total_tokens)}`);
422
- if (totalCost !== null) parts.push(`费用 ${formatCost(total.cost)}`);
423
- if (parts.length) console.log(` ${parts.join(' | ')}`);
424
- }
405
+ // 累计
406
+ if ((asNumber(total.requests) !== null || asNumber(total.cost) !== null) && total !== today) {
407
+ console.log('');
408
+ console.log('[累计]');
409
+ const parts = [];
410
+ if (asNumber(total.requests) !== null) parts.push(`请求 ${formatNumber(total.requests)} 次`);
411
+ if (asNumber(total.total_tokens) !== null) parts.push(`Token ${formatCompactNumber(total.total_tokens)}`);
412
+ if (asNumber(total.cost) !== null) parts.push(`费用 ${formatCost(total.cost)}`);
413
+ if (parts.length) console.log(` ${parts.join(' | ')}`);
425
414
  }
426
415
 
427
- const usage = stats.usage || {};
416
+ // 实时
428
417
  const rpm = asNumber(usage.rpm);
429
418
  const tpm = asNumber(usage.tpm);
430
- const avgDuration = asNumber(usage.average_duration_ms);
431
- if (rpm !== null || tpm !== null || avgDuration !== null) {
419
+ const avg = asNumber(usage.average_duration_ms);
420
+ if (rpm !== null || tpm !== null || avg !== null) {
421
+ console.log('');
422
+ console.log('[实时]');
432
423
  const parts = [];
433
424
  if (rpm !== null) parts.push(`RPM ${rpm}`);
434
- if (tpm !== null) parts.push(`TPM ${tpm}`);
435
- if (avgDuration !== null) parts.push(`平均耗时 ${avgDuration} ms`);
436
- console.log(`实时: ${parts.join(' | ')}`);
425
+ if (tpm !== null) parts.push(`TPM ${formatCompactNumber(tpm)}`);
426
+ if (avg !== null) parts.push(`平均 ${formatDuration(avg)}`);
427
+ console.log(` ${parts.join(' | ')}`);
437
428
  }
438
429
 
439
- if (Array.isArray(stats.model_stats) && stats.model_stats.length > 0) {
430
+ // 模型用量(按费用降序,列对齐)
431
+ const models = Array.isArray(stats.model_stats) ? stats.model_stats.slice() : [];
432
+ if (models.length) {
433
+ models.sort((a, b) => (asNumber(b.cost) || 0) - (asNumber(a.cost) || 0));
434
+ const nameWidth = Math.max(...models.map(m => String(m.model || '-').length));
435
+ const reqWidth = Math.max(...models.map(m => formatNumber(m.requests).length));
436
+ const tokWidth = Math.max(...models.map(m => formatCompactNumber(m.total_tokens).length));
437
+ const costWidth = Math.max(...models.map(m => formatCost(m.cost).length));
440
438
  console.log('');
441
- console.log('模型用量:');
442
- for (const m of stats.model_stats) {
443
- const parts = [
444
- `${m.model || '-'}`,
445
- `请求 ${formatNumber(m.requests)} 次`,
446
- `Token ${formatCompactNumber(m.total_tokens)}`,
447
- `费用 ${formatCost(m.cost)}`
448
- ];
449
- console.log(` - ${parts.join(' | ')}`);
439
+ console.log('[模型用量]');
440
+ for (const m of models) {
441
+ const name = String(m.model || '-').padEnd(nameWidth);
442
+ const req = formatNumber(m.requests).padStart(reqWidth);
443
+ const tok = formatCompactNumber(m.total_tokens).padStart(tokWidth);
444
+ const cost = formatCost(m.cost).padStart(costWidth);
445
+ console.log(` ${name} | ${req} 次 | ${tok} | ${cost}`);
450
446
  }
451
447
  }
452
448
 
@@ -534,11 +530,10 @@ async function usageCommand(args = []) {
534
530
  if (service.configs.length === 0) continue;
535
531
 
536
532
  hasAnyConfig = true;
537
- console.log('');
538
- console.log(`${'='.repeat(50)}`);
539
- console.log(`=== ${service.name} 用量统计 ===`);
540
- console.log(`${'='.repeat(50)}`);
541
- console.log('');
533
+ if (!outputJson) {
534
+ console.log('');
535
+ console.log(`════════ ${service.name} 用量 ════════`);
536
+ }
542
537
 
543
538
  allResults[service.name] = [];
544
539
 
@@ -549,9 +544,9 @@ async function usageCommand(args = []) {
549
544
  const requests = configsToUse.map(config => {
550
545
  const effectiveKey = overrideKey || config.authToken;
551
546
  const sourceLabel = overrideKey ? `命令行 Key (${config.source})` : config.source;
552
- console.log(`[查询中] ${sourceLabel} - Token: ${maskToken(effectiveKey)}`);
553
547
  return {
554
548
  sourceLabel,
549
+ tokenMask: maskToken(effectiveKey),
555
550
  promise: queryUnifiedUsage(config.baseUrl, effectiveKey)
556
551
  };
557
552
  });
@@ -560,21 +555,25 @@ async function usageCommand(args = []) {
560
555
 
561
556
  for (let i = 0; i < results.length; i += 1) {
562
557
  const outcome = results[i];
563
- const sourceLabel = requests[i].sourceLabel;
558
+ const { sourceLabel, tokenMask } = requests[i];
564
559
  const result = outcome.status === 'fulfilled'
565
560
  ? outcome.value
566
561
  : { error: outcome.reason ? outcome.reason.message : '查询失败' };
567
562
 
568
563
  if (result.error) {
569
- console.log(`[错误] ${result.error}`);
570
- console.log('');
564
+ if (!outputJson) {
565
+ console.log('');
566
+ console.log(`来源: ${sourceLabel} · ${tokenMask}`);
567
+ console.log(`[错误] ${result.error}`);
568
+ }
571
569
  allResults[service.name].push({
572
570
  source: sourceLabel,
573
571
  error: result.error
574
572
  });
575
573
  } else {
576
574
  if (!outputJson) {
577
- displayUnifiedUsageStats(result.stats, result.origin, sourceLabel);
575
+ console.log('');
576
+ displayUnifiedUsageStats(result.stats, result.origin, sourceLabel, tokenMask);
578
577
  }
579
578
  allResults[service.name].push({
580
579
  source: sourceLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aihezu",
3
- "version": "2.8.12",
3
+ "version": "2.8.13",
4
4
  "description": "AI 开发环境配置工具 - 支持 Claude Code, Codex, Google Gemini 的本地化配置、代理设置与缓存清理",
5
5
  "main": "bin/aihezu.js",
6
6
  "bin": {