aihezu 2.8.8 → 2.8.10
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/bin/aihezu.js +0 -1
- package/commands/usage.js +145 -177
- package/lib/http.js +57 -12
- package/package.json +1 -1
package/bin/aihezu.js
CHANGED
|
@@ -40,7 +40,6 @@ function showHelp() {
|
|
|
40
40
|
console.log(' aihezu usage 显示所有已配置服务的用量');
|
|
41
41
|
console.log(' aihezu usage cc 只显示 Claude Code 用量');
|
|
42
42
|
console.log(' aihezu usage codex 只显示 Codex 用量');
|
|
43
|
-
console.log(' aihezu usage gemini 只显示 Google Gemini 用量');
|
|
44
43
|
console.log(' aihezu usage --key sk-xxx 使用指定 API Key 查询');
|
|
45
44
|
console.log('\n服务:');
|
|
46
45
|
console.log(' claude Claude Code');
|
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 {
|
|
4
|
+
const { getJson } = require('../lib/http');
|
|
5
5
|
const platform = require('../lib/platform');
|
|
6
6
|
|
|
7
7
|
function normalizeHttpUrl(url) {
|
|
@@ -189,66 +189,6 @@ function readCodexConfig() {
|
|
|
189
189
|
return configs;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
// 读取 Gemini 配置
|
|
193
|
-
function readGeminiConfig() {
|
|
194
|
-
const configs = [];
|
|
195
|
-
|
|
196
|
-
// 读取环境变量
|
|
197
|
-
const envBaseUrl = process.env.GOOGLE_GEMINI_BASE_URL || '';
|
|
198
|
-
const envAuthToken = process.env.GEMINI_API_KEY || '';
|
|
199
|
-
|
|
200
|
-
if (envBaseUrl && envAuthToken) {
|
|
201
|
-
configs.push({
|
|
202
|
-
source: '环境变量',
|
|
203
|
-
baseUrl: normalizeHttpUrl(envBaseUrl),
|
|
204
|
-
authToken: envAuthToken.trim()
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 读取配置文件
|
|
209
|
-
const envFilePath = path.join(os.homedir(), '.gemini', '.env');
|
|
210
|
-
if (fs.existsSync(envFilePath)) {
|
|
211
|
-
try {
|
|
212
|
-
const content = fs.readFileSync(envFilePath, 'utf8');
|
|
213
|
-
const lines = content.split('\n');
|
|
214
|
-
|
|
215
|
-
let fileBaseUrl = '';
|
|
216
|
-
let fileAuthToken = '';
|
|
217
|
-
|
|
218
|
-
for (const line of lines) {
|
|
219
|
-
const match = line.match(/^(\w+)="?([^"]+)"?$/);
|
|
220
|
-
if (match) {
|
|
221
|
-
const [, key, value] = match;
|
|
222
|
-
if (key === 'GOOGLE_GEMINI_BASE_URL') {
|
|
223
|
-
fileBaseUrl = value;
|
|
224
|
-
} else if (key === 'GEMINI_API_KEY') {
|
|
225
|
-
fileAuthToken = value;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (fileBaseUrl && fileAuthToken) {
|
|
231
|
-
const isDuplicate = configs.some(c =>
|
|
232
|
-
c.baseUrl === normalizeHttpUrl(fileBaseUrl) &&
|
|
233
|
-
c.authToken === fileAuthToken.trim()
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
if (!isDuplicate) {
|
|
237
|
-
configs.push({
|
|
238
|
-
source: '配置文件',
|
|
239
|
-
baseUrl: normalizeHttpUrl(fileBaseUrl),
|
|
240
|
-
authToken: fileAuthToken.trim()
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} catch (error) {
|
|
245
|
-
// 忽略错误
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return configs;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
192
|
function asNumber(value) {
|
|
253
193
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
254
194
|
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
@@ -355,136 +295,169 @@ function usageHint(current, limit) {
|
|
|
355
295
|
return '';
|
|
356
296
|
}
|
|
357
297
|
|
|
358
|
-
|
|
298
|
+
function getUsageApiBase(baseUrl) {
|
|
299
|
+
// 去掉路径中的 /openai 后缀(Codex 的 base_url 常见格式 https://xxx/openai;
|
|
300
|
+
// Claude 的 base_url 不会带,等价于 no-op)
|
|
359
301
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const keyIdRes = await postJson(keyIdUrl, { apiKey: authToken });
|
|
365
|
-
if (keyIdRes.statusCode < 200 || keyIdRes.statusCode >= 300) {
|
|
366
|
-
return { error: `HTTP ${keyIdRes.statusCode}` };
|
|
302
|
+
const url = new URL(baseUrl);
|
|
303
|
+
let pathname = url.pathname.replace(/\/+$/, '');
|
|
304
|
+
if (pathname.toLowerCase().endsWith('/openai')) {
|
|
305
|
+
pathname = pathname.slice(0, -'/openai'.length);
|
|
367
306
|
}
|
|
307
|
+
return `${url.origin}${pathname}`;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return baseUrl.replace(/\/+$/, '').replace(/\/openai$/i, '');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
368
312
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
313
|
+
async function queryUnifiedUsage(baseUrl, authToken) {
|
|
314
|
+
try {
|
|
315
|
+
const apiBase = getUsageApiBase(baseUrl);
|
|
316
|
+
const usageUrl = `${apiBase}/v1/usage`;
|
|
317
|
+
const res = await getJson(usageUrl, {
|
|
318
|
+
headers: { Authorization: `Bearer ${authToken}` }
|
|
319
|
+
});
|
|
373
320
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
321
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
322
|
+
let detail = '';
|
|
323
|
+
const j = res.json;
|
|
324
|
+
if (j) {
|
|
325
|
+
if (typeof j.message === 'string') detail = j.message;
|
|
326
|
+
else if (typeof j.error === 'string') detail = j.error;
|
|
327
|
+
else if (j.error && typeof j.error.message === 'string') detail = j.error.message;
|
|
328
|
+
else if (typeof j.code === 'string') detail = j.code;
|
|
329
|
+
}
|
|
330
|
+
return { error: detail ? `HTTP ${res.statusCode} - ${detail}` : `HTTP ${res.statusCode}` };
|
|
377
331
|
}
|
|
378
332
|
|
|
379
|
-
if (!
|
|
333
|
+
if (!res.json || typeof res.json !== 'object') {
|
|
380
334
|
return { error: '返回内容不是合法的 JSON' };
|
|
381
335
|
}
|
|
382
336
|
|
|
383
|
-
|
|
384
|
-
return { stats, origin };
|
|
337
|
+
return { stats: res.json, origin: new URL(usageUrl).origin };
|
|
385
338
|
} catch (error) {
|
|
386
339
|
return { error: error.message };
|
|
387
340
|
}
|
|
388
341
|
}
|
|
389
342
|
|
|
390
|
-
function
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (provided !== null) return provided;
|
|
407
|
-
const endMs = normalizeEpochMs(windowEndTime);
|
|
408
|
-
if (endMs === null) return null;
|
|
409
|
-
return Math.max(0, Math.floor((endMs - Date.now()) / 1000));
|
|
410
|
-
})();
|
|
411
|
-
const windowDurationMinutes = (() => {
|
|
412
|
-
const startMs = normalizeEpochMs(windowStartTime);
|
|
413
|
-
const endMs = normalizeEpochMs(windowEndTime);
|
|
414
|
-
if (startMs !== null && endMs !== null && endMs > startMs) {
|
|
415
|
-
return Math.round((endMs - startMs) / 60000);
|
|
416
|
-
}
|
|
417
|
-
const raw = (stats.rateLimitWindow !== undefined) ? stats.rateLimitWindow : limits.rateLimitWindow;
|
|
418
|
-
const minutes = asNumber(raw);
|
|
419
|
-
if (minutes !== null) return minutes;
|
|
420
|
-
return null;
|
|
421
|
-
})();
|
|
422
|
-
|
|
423
|
-
const dailyRemaining = (() => {
|
|
424
|
-
const c = asNumber(currentDailyCost);
|
|
425
|
-
const l = asNumber(dailyCostLimit);
|
|
426
|
-
if (c === null || l === null) return null;
|
|
427
|
-
return l - c;
|
|
428
|
-
})();
|
|
429
|
-
|
|
430
|
-
const windowRemaining = (() => {
|
|
431
|
-
const c = asNumber(currentWindowCost);
|
|
432
|
-
const l = asNumber(rateLimitCost);
|
|
433
|
-
if (c === null || l === null) return null;
|
|
434
|
-
return l - c;
|
|
435
|
-
})();
|
|
436
|
-
|
|
437
|
-
// 提取日请求次数和 Token 用量(API 中 currentWindowRequests/Tokens 实际为日数据)
|
|
438
|
-
const currentDailyRequests = (stats.currentDailyRequests !== undefined) ? stats.currentDailyRequests
|
|
439
|
-
: (limits.currentDailyRequests !== undefined) ? limits.currentDailyRequests
|
|
440
|
-
: (stats.currentWindowRequests !== undefined) ? stats.currentWindowRequests : limits.currentWindowRequests;
|
|
441
|
-
const currentDailyTokens = (stats.currentDailyTokens !== undefined) ? stats.currentDailyTokens
|
|
442
|
-
: (limits.currentDailyTokens !== undefined) ? limits.currentDailyTokens
|
|
443
|
-
: (stats.currentWindowTokens !== undefined) ? stats.currentWindowTokens : limits.currentWindowTokens;
|
|
343
|
+
function displayUnifiedUsageStats(stats, origin, source) {
|
|
344
|
+
const subscription = stats.subscription || {};
|
|
345
|
+
const today = (stats.usage && stats.usage.today) || {};
|
|
346
|
+
const total = (stats.usage && stats.usage.total) || {};
|
|
347
|
+
|
|
348
|
+
const dailyUsage = asNumber(subscription.daily_usage_usd);
|
|
349
|
+
const dailyLimit = asNumber(subscription.daily_limit_usd);
|
|
350
|
+
const weeklyUsage = asNumber(subscription.weekly_usage_usd);
|
|
351
|
+
const weeklyLimit = asNumber(subscription.weekly_limit_usd);
|
|
352
|
+
const monthlyUsage = asNumber(subscription.monthly_usage_usd);
|
|
353
|
+
const monthlyLimit = asNumber(subscription.monthly_limit_usd);
|
|
354
|
+
|
|
355
|
+
const unit = stats.unit || 'USD';
|
|
356
|
+
const planName = stats.planName || '-';
|
|
357
|
+
const mode = stats.mode || '-';
|
|
358
|
+
const isValid = stats.isValid === false ? '否' : '是';
|
|
444
359
|
|
|
445
360
|
console.log(`域名: ${origin}`);
|
|
446
361
|
console.log(`来源: ${source}`);
|
|
362
|
+
console.log(`套餐: ${planName} | 模式: ${mode} | 有效: ${isValid} | 单位: ${unit}`);
|
|
447
363
|
console.log('');
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
364
|
+
|
|
365
|
+
if (dailyUsage !== null || dailyLimit !== null) {
|
|
366
|
+
const usageText = formatCost(dailyUsage);
|
|
367
|
+
if (dailyLimit !== null && dailyLimit > 0) {
|
|
368
|
+
console.log(
|
|
369
|
+
`日额度: ${usageText} / ${formatCost(dailyLimit)} (${formatPercent(dailyUsage, dailyLimit)}) ` +
|
|
370
|
+
`${renderBar(dailyUsage, dailyLimit)} ${usageHint(dailyUsage, dailyLimit)}`.trimEnd()
|
|
371
|
+
);
|
|
372
|
+
const dailyRemaining = asNumber(stats.remaining);
|
|
373
|
+
if (dailyRemaining !== null) {
|
|
374
|
+
console.log(`日剩余: ${formatCost(dailyRemaining)}`);
|
|
375
|
+
} else if (dailyUsage !== null) {
|
|
376
|
+
console.log(`日剩余: ${formatCost(dailyLimit - dailyUsage)}`);
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
console.log(`日用量: ${usageText} (无限制)`);
|
|
380
|
+
}
|
|
454
381
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
382
|
+
|
|
383
|
+
if (weeklyUsage !== null && weeklyLimit !== null && weeklyLimit > 0) {
|
|
384
|
+
console.log(
|
|
385
|
+
`周额度: ${formatCost(weeklyUsage)} / ${formatCost(weeklyLimit)} (${formatPercent(weeklyUsage, weeklyLimit)}) ` +
|
|
386
|
+
`${renderBar(weeklyUsage, weeklyLimit)} ${usageHint(weeklyUsage, weeklyLimit)}`.trimEnd()
|
|
387
|
+
);
|
|
388
|
+
} else if (weeklyUsage !== null && weeklyUsage > 0) {
|
|
389
|
+
console.log(`周用量: ${formatCost(weeklyUsage)}`);
|
|
460
390
|
}
|
|
461
391
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
console.log(
|
|
392
|
+
if (monthlyUsage !== null && monthlyLimit !== null && monthlyLimit > 0) {
|
|
393
|
+
console.log(
|
|
394
|
+
`月额度: ${formatCost(monthlyUsage)} / ${formatCost(monthlyLimit)} (${formatPercent(monthlyUsage, monthlyLimit)}) ` +
|
|
395
|
+
`${renderBar(monthlyUsage, monthlyLimit)} ${usageHint(monthlyUsage, monthlyLimit)}`.trimEnd()
|
|
396
|
+
);
|
|
397
|
+
} else if (monthlyUsage !== null && monthlyUsage > 0) {
|
|
398
|
+
console.log(`月用量: ${formatCost(monthlyUsage)}`);
|
|
469
399
|
}
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
console.log(`时间窗口开始: ${windowStartText}`);
|
|
477
|
-
} else {
|
|
478
|
-
console.log(`时间窗口结束: ${windowEndText}`);
|
|
400
|
+
|
|
401
|
+
const expiresAt = subscription.expires_at;
|
|
402
|
+
if (expiresAt) {
|
|
403
|
+
const expiresMs = Date.parse(expiresAt);
|
|
404
|
+
if (!Number.isNaN(expiresMs)) {
|
|
405
|
+
console.log(`套餐到期: ${formatDateTime(expiresMs)}`);
|
|
479
406
|
}
|
|
480
407
|
}
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
console.log(
|
|
408
|
+
|
|
409
|
+
if (Object.keys(today).length > 0) {
|
|
410
|
+
console.log('');
|
|
411
|
+
console.log('今日:');
|
|
412
|
+
const todayParts = [];
|
|
413
|
+
if (asNumber(today.requests) !== null) todayParts.push(`请求 ${formatNumber(today.requests)} 次`);
|
|
414
|
+
if (asNumber(today.total_tokens) !== null) todayParts.push(`Token ${formatCompactNumber(today.total_tokens)}`);
|
|
415
|
+
if (asNumber(today.input_tokens) !== null) todayParts.push(`输入 ${formatCompactNumber(today.input_tokens)}`);
|
|
416
|
+
if (asNumber(today.output_tokens) !== null) todayParts.push(`输出 ${formatCompactNumber(today.output_tokens)}`);
|
|
417
|
+
if (asNumber(today.cache_read_tokens) !== null) todayParts.push(`缓存读 ${formatCompactNumber(today.cache_read_tokens)}`);
|
|
418
|
+
if (asNumber(today.cache_creation_tokens) !== null) todayParts.push(`缓存写 ${formatCompactNumber(today.cache_creation_tokens)}`);
|
|
419
|
+
if (todayParts.length) console.log(` ${todayParts.join(' | ')}`);
|
|
420
|
+
if (asNumber(today.cost) !== null) console.log(` 费用: ${formatCost(today.cost)}`);
|
|
484
421
|
}
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
422
|
+
|
|
423
|
+
if (Object.keys(total).length > 0 && total !== today) {
|
|
424
|
+
const totalRequests = asNumber(total.requests);
|
|
425
|
+
const totalCost = asNumber(total.cost);
|
|
426
|
+
if (totalRequests !== null || totalCost !== null) {
|
|
427
|
+
console.log('');
|
|
428
|
+
console.log('累计:');
|
|
429
|
+
const parts = [];
|
|
430
|
+
if (totalRequests !== null) parts.push(`请求 ${formatNumber(total.requests)} 次`);
|
|
431
|
+
if (asNumber(total.total_tokens) !== null) parts.push(`Token ${formatCompactNumber(total.total_tokens)}`);
|
|
432
|
+
if (totalCost !== null) parts.push(`费用 ${formatCost(total.cost)}`);
|
|
433
|
+
if (parts.length) console.log(` ${parts.join(' | ')}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const usage = stats.usage || {};
|
|
438
|
+
const rpm = asNumber(usage.rpm);
|
|
439
|
+
const tpm = asNumber(usage.tpm);
|
|
440
|
+
const avgDuration = asNumber(usage.average_duration_ms);
|
|
441
|
+
if (rpm !== null || tpm !== null || avgDuration !== null) {
|
|
442
|
+
const parts = [];
|
|
443
|
+
if (rpm !== null) parts.push(`RPM ${rpm}`);
|
|
444
|
+
if (tpm !== null) parts.push(`TPM ${tpm}`);
|
|
445
|
+
if (avgDuration !== null) parts.push(`平均耗时 ${avgDuration} ms`);
|
|
446
|
+
console.log(`实时: ${parts.join(' | ')}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (Array.isArray(stats.model_stats) && stats.model_stats.length > 0) {
|
|
450
|
+
console.log('');
|
|
451
|
+
console.log('模型用量:');
|
|
452
|
+
for (const m of stats.model_stats) {
|
|
453
|
+
const parts = [
|
|
454
|
+
`${m.model || '-'}`,
|
|
455
|
+
`请求 ${formatNumber(m.requests)} 次`,
|
|
456
|
+
`Token ${formatCompactNumber(m.total_tokens)}`,
|
|
457
|
+
`费用 ${formatCost(m.cost)}`
|
|
458
|
+
];
|
|
459
|
+
console.log(` - ${parts.join(' | ')}`);
|
|
460
|
+
}
|
|
488
461
|
}
|
|
489
462
|
|
|
490
463
|
console.log('');
|
|
@@ -494,7 +467,7 @@ function showUsageHelp() {
|
|
|
494
467
|
console.log('用法: aihezu usage [service] [--json] [--key <apiKey>]');
|
|
495
468
|
console.log('');
|
|
496
469
|
console.log('说明:');
|
|
497
|
-
console.log(' - 自动检测 Claude Code、Codex
|
|
470
|
+
console.log(' - 自动检测 Claude Code、Codex 的配置');
|
|
498
471
|
console.log(' - 从环境变量和配置文件读取配置');
|
|
499
472
|
console.log(' - 如果两者都存在且不同,将分别查询两个账号的用量');
|
|
500
473
|
console.log(' - 使用 --key 可查询指定 API Key 的用量');
|
|
@@ -502,7 +475,6 @@ function showUsageHelp() {
|
|
|
502
475
|
console.log('可选服务参数:');
|
|
503
476
|
console.log(' cc, claude 只显示 Claude Code 用量');
|
|
504
477
|
console.log(' codex 只显示 Codex 用量');
|
|
505
|
-
console.log(' gemini 只显示 Google Gemini 用量');
|
|
506
478
|
console.log(' (不指定) 显示所有已配置服务的用量');
|
|
507
479
|
console.log('');
|
|
508
480
|
console.log('可选参数:');
|
|
@@ -513,7 +485,6 @@ function showUsageHelp() {
|
|
|
513
485
|
console.log(' npx aihezu usage # 显示所有服务');
|
|
514
486
|
console.log(' npx aihezu usage cc # 只显示 Claude Code');
|
|
515
487
|
console.log(' npx aihezu usage codex # 只显示 Codex');
|
|
516
|
-
console.log(' npx aihezu usage gemini # 只显示 Gemini');
|
|
517
488
|
console.log(' npx aihezu usage --json # JSON 格式输出所有服务');
|
|
518
489
|
console.log(' npx aihezu usage cc --json # JSON 格式输出 Claude Code');
|
|
519
490
|
console.log(' npx aihezu usage --key sk-xxx # 使用指定 Key 查询');
|
|
@@ -537,8 +508,7 @@ async function usageCommand(args = []) {
|
|
|
537
508
|
const serviceAliases = {
|
|
538
509
|
'cc': 'Claude Code',
|
|
539
510
|
'claude': 'Claude Code',
|
|
540
|
-
'codex': 'Codex'
|
|
541
|
-
'gemini': 'Google Gemini'
|
|
511
|
+
'codex': 'Codex'
|
|
542
512
|
};
|
|
543
513
|
|
|
544
514
|
let filterServiceName = null;
|
|
@@ -549,7 +519,7 @@ async function usageCommand(args = []) {
|
|
|
549
519
|
if (!filterServiceName) {
|
|
550
520
|
console.error(`[错误] 未知的服务: ${serviceFilter}`);
|
|
551
521
|
console.error('');
|
|
552
|
-
console.error('可用服务: cc, claude, codex
|
|
522
|
+
console.error('可用服务: cc, claude, codex');
|
|
553
523
|
console.error('');
|
|
554
524
|
console.error('使用 "aihezu usage --help" 查看帮助');
|
|
555
525
|
process.exit(1);
|
|
@@ -559,8 +529,7 @@ async function usageCommand(args = []) {
|
|
|
559
529
|
// 收集所有服务的配置
|
|
560
530
|
const allServices = [
|
|
561
531
|
{ name: 'Claude Code', configs: readClaudeConfig() },
|
|
562
|
-
{ name: 'Codex', configs: readCodexConfig() }
|
|
563
|
-
{ name: 'Google Gemini', configs: readGeminiConfig() }
|
|
532
|
+
{ name: 'Codex', configs: readCodexConfig() }
|
|
564
533
|
];
|
|
565
534
|
|
|
566
535
|
// 根据过滤参数筛选服务
|
|
@@ -593,7 +562,7 @@ async function usageCommand(args = []) {
|
|
|
593
562
|
console.log(`[查询中] ${sourceLabel} - Token: ${maskToken(effectiveKey)}`);
|
|
594
563
|
return {
|
|
595
564
|
sourceLabel,
|
|
596
|
-
promise:
|
|
565
|
+
promise: queryUnifiedUsage(config.baseUrl, effectiveKey)
|
|
597
566
|
};
|
|
598
567
|
});
|
|
599
568
|
|
|
@@ -615,7 +584,7 @@ async function usageCommand(args = []) {
|
|
|
615
584
|
});
|
|
616
585
|
} else {
|
|
617
586
|
if (!outputJson) {
|
|
618
|
-
|
|
587
|
+
displayUnifiedUsageStats(result.stats, result.origin, sourceLabel);
|
|
619
588
|
}
|
|
620
589
|
allResults[service.name].push({
|
|
621
590
|
source: sourceLabel,
|
|
@@ -637,7 +606,6 @@ async function usageCommand(args = []) {
|
|
|
637
606
|
console.log('请先运行以下命令进行配置:');
|
|
638
607
|
console.log(' npx aihezu install claude # 配置 Claude Code');
|
|
639
608
|
console.log(' npx aihezu install codex # 配置 Codex');
|
|
640
|
-
console.log(' npx aihezu install gemini # 配置 Google Gemini');
|
|
641
609
|
console.log('');
|
|
642
610
|
console.log('或者手动设置环境变量:');
|
|
643
611
|
console.log('');
|
package/lib/http.js
CHANGED
|
@@ -8,11 +8,22 @@ function sleep(ms) {
|
|
|
8
8
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function sendJsonRequest(method, urlString, body, timeoutMs) {
|
|
11
|
+
function sendJsonRequest(method, urlString, body, timeoutMs, extraHeaders = {}) {
|
|
12
12
|
const url = new URL(urlString);
|
|
13
|
-
const
|
|
13
|
+
const hasBody = body !== undefined && body !== null;
|
|
14
|
+
const payload = hasBody ? JSON.stringify(body) : '';
|
|
14
15
|
const transport = url.protocol === 'https:' ? https : http;
|
|
15
16
|
|
|
17
|
+
const headers = {
|
|
18
|
+
'Accept': 'application/json',
|
|
19
|
+
'User-Agent': 'aihezu-cli',
|
|
20
|
+
...extraHeaders
|
|
21
|
+
};
|
|
22
|
+
if (hasBody) {
|
|
23
|
+
headers['Content-Type'] = 'application/json';
|
|
24
|
+
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
return new Promise((resolve, reject) => {
|
|
17
28
|
const req = transport.request(
|
|
18
29
|
{
|
|
@@ -20,12 +31,7 @@ function sendJsonRequest(method, urlString, body, timeoutMs) {
|
|
|
20
31
|
hostname: url.hostname,
|
|
21
32
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
22
33
|
path: `${url.pathname}${url.search || ''}`,
|
|
23
|
-
headers
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
'Accept': 'application/json',
|
|
26
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
27
|
-
'User-Agent': 'aihezu-cli'
|
|
28
|
-
}
|
|
34
|
+
headers
|
|
29
35
|
},
|
|
30
36
|
res => {
|
|
31
37
|
res.setEncoding('utf8');
|
|
@@ -57,7 +63,9 @@ function sendJsonRequest(method, urlString, body, timeoutMs) {
|
|
|
57
63
|
req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
58
64
|
});
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
if (hasBody) {
|
|
67
|
+
req.write(payload);
|
|
68
|
+
}
|
|
61
69
|
req.end();
|
|
62
70
|
});
|
|
63
71
|
}
|
|
@@ -67,7 +75,43 @@ async function postJson(urlString, body, options = {}) {
|
|
|
67
75
|
timeoutMs = HTTP_TIMEOUT,
|
|
68
76
|
retryCount = HTTP_RETRY_COUNT,
|
|
69
77
|
retryDelayMs = HTTP_RETRY_DELAY,
|
|
70
|
-
retryStatusCodes = DEFAULT_RETRY_STATUS_CODES
|
|
78
|
+
retryStatusCodes = DEFAULT_RETRY_STATUS_CODES,
|
|
79
|
+
headers = {}
|
|
80
|
+
} = options;
|
|
81
|
+
|
|
82
|
+
const retryCodes = retryStatusCodes instanceof Set ? retryStatusCodes : new Set(retryStatusCodes);
|
|
83
|
+
let attempt = 0;
|
|
84
|
+
let lastError = null;
|
|
85
|
+
|
|
86
|
+
while (attempt <= retryCount) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await sendJsonRequest('POST', urlString, body, timeoutMs, headers);
|
|
89
|
+
if (retryCodes.has(response.statusCode) && attempt < retryCount) {
|
|
90
|
+
await sleep(retryDelayMs * Math.pow(2, attempt));
|
|
91
|
+
attempt += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
return response;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
lastError = error;
|
|
97
|
+
if (attempt >= retryCount) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
await sleep(retryDelayMs * Math.pow(2, attempt));
|
|
101
|
+
attempt += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
throw lastError || new Error('Request failed');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getJson(urlString, options = {}) {
|
|
109
|
+
const {
|
|
110
|
+
timeoutMs = HTTP_TIMEOUT,
|
|
111
|
+
retryCount = HTTP_RETRY_COUNT,
|
|
112
|
+
retryDelayMs = HTTP_RETRY_DELAY,
|
|
113
|
+
retryStatusCodes = DEFAULT_RETRY_STATUS_CODES,
|
|
114
|
+
headers = {}
|
|
71
115
|
} = options;
|
|
72
116
|
|
|
73
117
|
const retryCodes = retryStatusCodes instanceof Set ? retryStatusCodes : new Set(retryStatusCodes);
|
|
@@ -76,7 +120,7 @@ async function postJson(urlString, body, options = {}) {
|
|
|
76
120
|
|
|
77
121
|
while (attempt <= retryCount) {
|
|
78
122
|
try {
|
|
79
|
-
const response = await sendJsonRequest('
|
|
123
|
+
const response = await sendJsonRequest('GET', urlString, null, timeoutMs, headers);
|
|
80
124
|
if (retryCodes.has(response.statusCode) && attempt < retryCount) {
|
|
81
125
|
await sleep(retryDelayMs * Math.pow(2, attempt));
|
|
82
126
|
attempt += 1;
|
|
@@ -97,5 +141,6 @@ async function postJson(urlString, body, options = {}) {
|
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
module.exports = {
|
|
100
|
-
postJson
|
|
144
|
+
postJson,
|
|
145
|
+
getJson
|
|
101
146
|
};
|