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 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 { postJson } = require('../lib/http');
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
- async function queryUsage(baseUrl, authToken) {
298
+ function getUsageApiBase(baseUrl) {
299
+ // 去掉路径中的 /openai 后缀(Codex 的 base_url 常见格式 https://xxx/openai;
300
+ // Claude 的 base_url 不会带,等价于 no-op)
359
301
  try {
360
- const origin = new URL(baseUrl).origin;
361
- const keyIdUrl = `${origin}/apiStats/api/get-key-id`;
362
- const statsUrl = `${origin}/apiStats/api/user-stats`;
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
- const apiId = keyIdRes?.json?.data?.id || keyIdRes?.json?.id || null;
370
- if (!apiId) {
371
- return { error: '未找到 API Key ID' };
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
- const statsRes = await postJson(statsUrl, { apiId });
375
- if (statsRes.statusCode < 200 || statsRes.statusCode >= 300) {
376
- return { error: `HTTP ${statsRes.statusCode}` };
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 (!statsRes.json || typeof statsRes.json !== 'object') {
333
+ if (!res.json || typeof res.json !== 'object') {
380
334
  return { error: '返回内容不是合法的 JSON' };
381
335
  }
382
336
 
383
- const stats = (statsRes?.json?.data) ? statsRes.json.data : (statsRes.json || {});
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 displayUsageStats(stats, origin, source) {
391
- const limits = (stats && typeof stats === 'object' && stats.limits && typeof stats.limits === 'object')
392
- ? stats.limits
393
- : {};
394
-
395
- const currentDailyCost = (stats.currentDailyCost !== undefined) ? stats.currentDailyCost : limits.currentDailyCost;
396
- const dailyCostLimit = (stats.dailyCostLimit !== undefined) ? stats.dailyCostLimit : limits.dailyCostLimit;
397
- const currentWindowCost = (stats.currentWindowCost !== undefined) ? stats.currentWindowCost : limits.currentWindowCost;
398
- const rateLimitCost = (stats.rateLimitCost !== undefined) ? stats.rateLimitCost : limits.rateLimitCost;
399
- const windowStartTime = (stats.windowStartTime !== undefined) ? stats.windowStartTime : limits.windowStartTime;
400
- const windowEndTime = (stats.windowEndTime !== undefined) ? stats.windowEndTime : limits.windowEndTime;
401
- const windowRemainingSeconds = (() => {
402
- const raw = (stats.windowRemainingSeconds !== undefined)
403
- ? stats.windowRemainingSeconds
404
- : limits.windowRemainingSeconds;
405
- const provided = asNumber(raw);
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
- console.log(
449
- `日额度: ${formatCost(currentDailyCost)} / ${formatCost(dailyCostLimit)} (${formatPercent(currentDailyCost, dailyCostLimit)}) ` +
450
- `${renderBar(currentDailyCost, dailyCostLimit)} ${usageHint(currentDailyCost, dailyCostLimit)}`.trimEnd()
451
- );
452
- if (dailyRemaining !== null) {
453
- console.log(`日剩余: ${formatCost(dailyRemaining)}`);
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
- if (asNumber(currentDailyRequests) !== null || asNumber(currentDailyTokens) !== null) {
456
- const parts = [];
457
- if (asNumber(currentDailyRequests) !== null) parts.push(`请求 ${formatNumber(currentDailyRequests)} 次`);
458
- if (asNumber(currentDailyTokens) !== null) parts.push(`Token ${formatCompactNumber(currentDailyTokens)}`);
459
- console.log(`日统计: ${parts.join(' | ')}`);
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
- console.log('');
463
- console.log(
464
- `窗口: ${formatCost(currentWindowCost)} / ${formatCost(rateLimitCost)} (${formatPercent(currentWindowCost, rateLimitCost)}) ` +
465
- `${renderBar(currentWindowCost, rateLimitCost)} ${usageHint(currentWindowCost, rateLimitCost)}`.trimEnd()
466
- );
467
- if (windowRemaining !== null) {
468
- console.log(`窗口剩余: ${formatCost(windowRemaining)}`);
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
- const windowStartText = formatDateTime(windowStartTime);
471
- const windowEndText = formatDateTime(windowEndTime);
472
- if (windowStartText !== '-' || windowEndText !== '-') {
473
- if (windowStartText !== '-' && windowEndText !== '-') {
474
- console.log(`时间窗口: ${windowStartText} ~ ${windowEndText}`);
475
- } else if (windowStartText !== '-') {
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
- const windowDurationText = formatDurationMinutes(windowDurationMinutes);
482
- if (windowDurationText !== '-') {
483
- console.log(`周时间窗口: ${windowDurationText}`);
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
- const resetCountdownText = formatDurationSeconds(windowRemainingSeconds);
486
- if (resetCountdownText !== '-') {
487
- console.log(`窗口剩余时间: ${resetCountdownText} (距重置)`);
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、Google Gemini 的配置');
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, gemini');
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: queryUsage(config.baseUrl, effectiveKey)
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
- displayUsageStats(result.stats, result.origin, sourceLabel);
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 payload = JSON.stringify(body ?? {});
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
- req.write(payload);
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('POST', urlString, body, timeoutMs);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aihezu",
3
- "version": "2.8.8",
3
+ "version": "2.8.10",
4
4
  "description": "AI 开发环境配置工具 - 支持 Claude Code, Codex, Google Gemini 的本地化配置、代理设置与缓存清理",
5
5
  "main": "bin/aihezu.js",
6
6
  "bin": {