aihezu 2.3.4 → 2.5.0
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 +13 -6
- package/commands/usage.js +497 -0
- package/package.json +1 -1
- package/commands/ccusage.js +0 -302
package/bin/aihezu.js
CHANGED
|
@@ -7,7 +7,7 @@ const readline = require('readline');
|
|
|
7
7
|
const commands = {
|
|
8
8
|
install: require('../commands/install'),
|
|
9
9
|
clear: require('../commands/clear'),
|
|
10
|
-
|
|
10
|
+
usage: require('../commands/usage')
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const services = {
|
|
@@ -23,7 +23,13 @@ function showHelp() {
|
|
|
23
23
|
console.log('\n命令:');
|
|
24
24
|
console.log(' install <service> 配置服务 (API Key, URL, 网络设置)');
|
|
25
25
|
console.log(' clear <service> 清理缓存并刷新网络设置');
|
|
26
|
-
console.log('
|
|
26
|
+
console.log(' usage [service] 查看用量统计');
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log('usage 命令可选参数:');
|
|
29
|
+
console.log(' aihezu usage 显示所有已配置服务的用量');
|
|
30
|
+
console.log(' aihezu usage cc 只显示 Claude Code 用量');
|
|
31
|
+
console.log(' aihezu usage codex 只显示 Codex 用量');
|
|
32
|
+
console.log(' aihezu usage gemini 只显示 Google Gemini 用量');
|
|
27
33
|
console.log('\n服务:');
|
|
28
34
|
console.log(' claude Claude Code');
|
|
29
35
|
console.log(' codex Codex');
|
|
@@ -41,9 +47,10 @@ function showHelp() {
|
|
|
41
47
|
console.log('\n示例:');
|
|
42
48
|
console.log(' aihezu install claude');
|
|
43
49
|
console.log(' aihezu clear codex');
|
|
44
|
-
console.log(' aihezu
|
|
45
|
-
console.log(' aihezu
|
|
46
|
-
console.log(' aihezu
|
|
50
|
+
console.log(' aihezu usage (查看所有服务用量)');
|
|
51
|
+
console.log(' aihezu usage cc (只查看 Claude Code 用量)');
|
|
52
|
+
console.log(' aihezu install (交互式安装)');
|
|
53
|
+
console.log(' aihezu help (显示帮助信息)');
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
async function askQuestion(rl, question) {
|
|
@@ -65,7 +72,7 @@ async function main() {
|
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
// Commands that don't require a service
|
|
68
|
-
if (commandName === '
|
|
75
|
+
if (commandName === 'usage') {
|
|
69
76
|
await commands[commandName](args.slice(1));
|
|
70
77
|
return;
|
|
71
78
|
}
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
|
|
7
|
+
function normalizeHttpUrl(url) {
|
|
8
|
+
if (!url) return '';
|
|
9
|
+
const trimmed = String(url).trim();
|
|
10
|
+
if (!trimmed) return '';
|
|
11
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
12
|
+
return `https://${trimmed}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function maskToken(token) {
|
|
16
|
+
if (!token || token.length <= 8) return '***';
|
|
17
|
+
return '***' + token.slice(-8);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 读取 Claude 配置
|
|
21
|
+
function readClaudeConfig() {
|
|
22
|
+
const configs = [];
|
|
23
|
+
|
|
24
|
+
// 读取环境变量
|
|
25
|
+
const envBaseUrl = process.env.ANTHROPIC_BASE_URL || '';
|
|
26
|
+
const envAuthToken = process.env.ANTHROPIC_AUTH_TOKEN || '';
|
|
27
|
+
|
|
28
|
+
if (envBaseUrl && envAuthToken) {
|
|
29
|
+
configs.push({
|
|
30
|
+
source: '环境变量',
|
|
31
|
+
baseUrl: normalizeHttpUrl(envBaseUrl),
|
|
32
|
+
authToken: envAuthToken.trim()
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 读取配置文件
|
|
37
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
38
|
+
if (fs.existsSync(settingsPath)) {
|
|
39
|
+
try {
|
|
40
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
41
|
+
const settings = JSON.parse(content);
|
|
42
|
+
const fileBaseUrl = settings?.env?.ANTHROPIC_BASE_URL || '';
|
|
43
|
+
const fileAuthToken = settings?.env?.ANTHROPIC_AUTH_TOKEN || '';
|
|
44
|
+
|
|
45
|
+
if (fileBaseUrl && fileAuthToken) {
|
|
46
|
+
// 检查是否与环境变量配置相同
|
|
47
|
+
const isDuplicate = configs.some(c =>
|
|
48
|
+
c.baseUrl === normalizeHttpUrl(fileBaseUrl) &&
|
|
49
|
+
c.authToken === fileAuthToken.trim()
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!isDuplicate) {
|
|
53
|
+
configs.push({
|
|
54
|
+
source: '配置文件',
|
|
55
|
+
baseUrl: normalizeHttpUrl(fileBaseUrl),
|
|
56
|
+
authToken: fileAuthToken.trim()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// 忽略错误
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return configs;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 读取 Codex 配置
|
|
69
|
+
function readCodexConfig() {
|
|
70
|
+
const configs = [];
|
|
71
|
+
|
|
72
|
+
// 读取环境变量
|
|
73
|
+
const envAuthToken = process.env.AIHEZU_OAI_KEY || '';
|
|
74
|
+
|
|
75
|
+
// 读取配置文件中的 base_url
|
|
76
|
+
let fileBaseUrl = '';
|
|
77
|
+
let fileAuthToken = '';
|
|
78
|
+
|
|
79
|
+
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
80
|
+
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(configPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
85
|
+
// 提取 base_url
|
|
86
|
+
const baseUrlMatch = content.match(/base_url\s*=\s*"([^"]+)"/);
|
|
87
|
+
if (baseUrlMatch) {
|
|
88
|
+
fileBaseUrl = baseUrlMatch[1];
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// 忽略错误
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (fs.existsSync(authPath)) {
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(authPath, 'utf8');
|
|
98
|
+
const authData = JSON.parse(content);
|
|
99
|
+
fileAuthToken = authData.AIHEZU_OAI_KEY || '';
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// 忽略错误
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 环境变量配置(使用配置文件的 base_url)
|
|
106
|
+
if (envAuthToken && fileBaseUrl) {
|
|
107
|
+
configs.push({
|
|
108
|
+
source: '环境变量',
|
|
109
|
+
baseUrl: normalizeHttpUrl(fileBaseUrl),
|
|
110
|
+
authToken: envAuthToken.trim()
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 配置文件配置
|
|
115
|
+
if (fileAuthToken && fileBaseUrl) {
|
|
116
|
+
const isDuplicate = configs.some(c => c.authToken === fileAuthToken.trim());
|
|
117
|
+
if (!isDuplicate) {
|
|
118
|
+
configs.push({
|
|
119
|
+
source: '配置文件',
|
|
120
|
+
baseUrl: normalizeHttpUrl(fileBaseUrl),
|
|
121
|
+
authToken: fileAuthToken.trim()
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return configs;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 读取 Gemini 配置
|
|
130
|
+
function readGeminiConfig() {
|
|
131
|
+
const configs = [];
|
|
132
|
+
|
|
133
|
+
// 读取环境变量
|
|
134
|
+
const envBaseUrl = process.env.GOOGLE_GEMINI_BASE_URL || '';
|
|
135
|
+
const envAuthToken = process.env.GEMINI_API_KEY || '';
|
|
136
|
+
|
|
137
|
+
if (envBaseUrl && envAuthToken) {
|
|
138
|
+
configs.push({
|
|
139
|
+
source: '环境变量',
|
|
140
|
+
baseUrl: normalizeHttpUrl(envBaseUrl),
|
|
141
|
+
authToken: envAuthToken.trim()
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 读取配置文件
|
|
146
|
+
const envFilePath = path.join(os.homedir(), '.gemini', '.env');
|
|
147
|
+
if (fs.existsSync(envFilePath)) {
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(envFilePath, 'utf8');
|
|
150
|
+
const lines = content.split('\n');
|
|
151
|
+
|
|
152
|
+
let fileBaseUrl = '';
|
|
153
|
+
let fileAuthToken = '';
|
|
154
|
+
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
const match = line.match(/^(\w+)="?([^"]+)"?$/);
|
|
157
|
+
if (match) {
|
|
158
|
+
const [, key, value] = match;
|
|
159
|
+
if (key === 'GOOGLE_GEMINI_BASE_URL') {
|
|
160
|
+
fileBaseUrl = value;
|
|
161
|
+
} else if (key === 'GEMINI_API_KEY') {
|
|
162
|
+
fileAuthToken = value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (fileBaseUrl && fileAuthToken) {
|
|
168
|
+
const isDuplicate = configs.some(c =>
|
|
169
|
+
c.baseUrl === normalizeHttpUrl(fileBaseUrl) &&
|
|
170
|
+
c.authToken === fileAuthToken.trim()
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (!isDuplicate) {
|
|
174
|
+
configs.push({
|
|
175
|
+
source: '配置文件',
|
|
176
|
+
baseUrl: normalizeHttpUrl(fileBaseUrl),
|
|
177
|
+
authToken: fileAuthToken.trim()
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// 忽略错误
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return configs;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function postJson(urlString, body, options = {}) {
|
|
190
|
+
const { timeoutMs = 15000 } = options;
|
|
191
|
+
const url = new URL(urlString);
|
|
192
|
+
const payload = JSON.stringify(body ?? {});
|
|
193
|
+
|
|
194
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
195
|
+
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const req = transport.request(
|
|
198
|
+
{
|
|
199
|
+
method: 'POST',
|
|
200
|
+
hostname: url.hostname,
|
|
201
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
202
|
+
path: `${url.pathname}${url.search || ''}`,
|
|
203
|
+
headers: {
|
|
204
|
+
'Content-Type': 'application/json',
|
|
205
|
+
'Accept': 'application/json',
|
|
206
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
207
|
+
'User-Agent': 'aihezu-cli'
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
res => {
|
|
211
|
+
res.setEncoding('utf8');
|
|
212
|
+
let raw = '';
|
|
213
|
+
|
|
214
|
+
res.on('data', chunk => {
|
|
215
|
+
raw += chunk;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
res.on('end', () => {
|
|
219
|
+
let json = null;
|
|
220
|
+
try {
|
|
221
|
+
json = raw ? JSON.parse(raw) : null;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// keep json as null, return raw for debugging
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
resolve({
|
|
227
|
+
statusCode: res.statusCode || 0,
|
|
228
|
+
raw,
|
|
229
|
+
json
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
req.on('error', reject);
|
|
236
|
+
req.setTimeout(timeoutMs, () => {
|
|
237
|
+
req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
req.write(payload);
|
|
241
|
+
req.end();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function asNumber(value) {
|
|
246
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
247
|
+
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
248
|
+
return Number(value);
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function formatCost(value) {
|
|
254
|
+
const n = asNumber(value);
|
|
255
|
+
if (n === null) return '-';
|
|
256
|
+
return `$${n.toFixed(2)}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function formatPercent(current, limit) {
|
|
260
|
+
const c = asNumber(current);
|
|
261
|
+
const l = asNumber(limit);
|
|
262
|
+
if (c === null || l === null || l <= 0) return '-';
|
|
263
|
+
return `${((c / l) * 100).toFixed(1)}%`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderBar(current, limit, width = 18) {
|
|
267
|
+
const c = asNumber(current);
|
|
268
|
+
const l = asNumber(limit);
|
|
269
|
+
if (c === null || l === null || l <= 0) return '';
|
|
270
|
+
|
|
271
|
+
const ratio = Math.max(0, Math.min(1, c / l));
|
|
272
|
+
let filled = Math.round(ratio * width);
|
|
273
|
+
if (ratio > 0 && filled === 0) filled = 1;
|
|
274
|
+
const empty = Math.max(0, width - filled);
|
|
275
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function usageHint(current, limit) {
|
|
279
|
+
const c = asNumber(current);
|
|
280
|
+
const l = asNumber(limit);
|
|
281
|
+
if (c === null || l === null || l <= 0) return '';
|
|
282
|
+
const ratio = c / l;
|
|
283
|
+
if (ratio >= 1) return '[!] 已超出限制';
|
|
284
|
+
if (ratio >= 0.9) return '[!] 接近上限';
|
|
285
|
+
return '';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function queryUsage(baseUrl, authToken) {
|
|
289
|
+
try {
|
|
290
|
+
const origin = new URL(baseUrl).origin;
|
|
291
|
+
const keyIdUrl = `${origin}/apiStats/api/get-key-id`;
|
|
292
|
+
const statsUrl = `${origin}/apiStats/api/user-stats`;
|
|
293
|
+
|
|
294
|
+
const keyIdRes = await postJson(keyIdUrl, { apiKey: authToken });
|
|
295
|
+
if (keyIdRes.statusCode < 200 || keyIdRes.statusCode >= 300) {
|
|
296
|
+
return { error: `HTTP ${keyIdRes.statusCode}` };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const apiId = keyIdRes?.json?.data?.id || keyIdRes?.json?.id || null;
|
|
300
|
+
if (!apiId) {
|
|
301
|
+
return { error: '未找到 API Key ID' };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const statsRes = await postJson(statsUrl, { apiId });
|
|
305
|
+
if (statsRes.statusCode < 200 || statsRes.statusCode >= 300) {
|
|
306
|
+
return { error: `HTTP ${statsRes.statusCode}` };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!statsRes.json || typeof statsRes.json !== 'object') {
|
|
310
|
+
return { error: '返回内容不是合法的 JSON' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const stats = (statsRes?.json?.data) ? statsRes.json.data : (statsRes.json || {});
|
|
314
|
+
return { stats, origin };
|
|
315
|
+
} catch (error) {
|
|
316
|
+
return { error: error.message };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function displayUsageStats(stats, origin, source) {
|
|
321
|
+
const limits = (stats && typeof stats === 'object' && stats.limits && typeof stats.limits === 'object')
|
|
322
|
+
? stats.limits
|
|
323
|
+
: {};
|
|
324
|
+
|
|
325
|
+
const currentDailyCost = (stats.currentDailyCost !== undefined) ? stats.currentDailyCost : limits.currentDailyCost;
|
|
326
|
+
const dailyCostLimit = (stats.dailyCostLimit !== undefined) ? stats.dailyCostLimit : limits.dailyCostLimit;
|
|
327
|
+
const currentWindowCost = (stats.currentWindowCost !== undefined) ? stats.currentWindowCost : limits.currentWindowCost;
|
|
328
|
+
const rateLimitCost = (stats.rateLimitCost !== undefined) ? stats.rateLimitCost : limits.rateLimitCost;
|
|
329
|
+
|
|
330
|
+
const dailyRemaining = (() => {
|
|
331
|
+
const c = asNumber(currentDailyCost);
|
|
332
|
+
const l = asNumber(dailyCostLimit);
|
|
333
|
+
if (c === null || l === null) return null;
|
|
334
|
+
return l - c;
|
|
335
|
+
})();
|
|
336
|
+
|
|
337
|
+
const windowRemaining = (() => {
|
|
338
|
+
const c = asNumber(currentWindowCost);
|
|
339
|
+
const l = asNumber(rateLimitCost);
|
|
340
|
+
if (c === null || l === null) return null;
|
|
341
|
+
return l - c;
|
|
342
|
+
})();
|
|
343
|
+
|
|
344
|
+
console.log(`域名: ${origin}`);
|
|
345
|
+
console.log(`来源: ${source}`);
|
|
346
|
+
console.log('');
|
|
347
|
+
console.log(
|
|
348
|
+
`日额度: ${formatCost(currentDailyCost)} / ${formatCost(dailyCostLimit)} (${formatPercent(currentDailyCost, dailyCostLimit)}) ` +
|
|
349
|
+
`${renderBar(currentDailyCost, dailyCostLimit)} ${usageHint(currentDailyCost, dailyCostLimit)}`.trimEnd()
|
|
350
|
+
);
|
|
351
|
+
if (dailyRemaining !== null) {
|
|
352
|
+
console.log(`日剩余: ${formatCost(dailyRemaining)}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log('');
|
|
356
|
+
console.log(
|
|
357
|
+
`窗口: ${formatCost(currentWindowCost)} / ${formatCost(rateLimitCost)} (${formatPercent(currentWindowCost, rateLimitCost)}) ` +
|
|
358
|
+
`${renderBar(currentWindowCost, rateLimitCost)} ${usageHint(currentWindowCost, rateLimitCost)}`.trimEnd()
|
|
359
|
+
);
|
|
360
|
+
if (windowRemaining !== null) {
|
|
361
|
+
console.log(`窗口剩余: ${formatCost(windowRemaining)}`);
|
|
362
|
+
}
|
|
363
|
+
console.log('');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function showUsageHelp() {
|
|
367
|
+
console.log('用法: aihezu usage [service] [--json]');
|
|
368
|
+
console.log('');
|
|
369
|
+
console.log('说明:');
|
|
370
|
+
console.log(' - 自动检测 Claude Code、Codex、Google Gemini 的配置');
|
|
371
|
+
console.log(' - 从环境变量和配置文件读取配置');
|
|
372
|
+
console.log(' - 如果两者都存在且不同,将分别查询两个账号的用量');
|
|
373
|
+
console.log('');
|
|
374
|
+
console.log('可选服务参数:');
|
|
375
|
+
console.log(' cc, claude 只显示 Claude Code 用量');
|
|
376
|
+
console.log(' codex 只显示 Codex 用量');
|
|
377
|
+
console.log(' gemini 只显示 Google Gemini 用量');
|
|
378
|
+
console.log(' (不指定) 显示所有已配置服务的用量');
|
|
379
|
+
console.log('');
|
|
380
|
+
console.log('示例:');
|
|
381
|
+
console.log(' npx aihezu usage # 显示所有服务');
|
|
382
|
+
console.log(' npx aihezu usage cc # 只显示 Claude Code');
|
|
383
|
+
console.log(' npx aihezu usage codex # 只显示 Codex');
|
|
384
|
+
console.log(' npx aihezu usage gemini # 只显示 Gemini');
|
|
385
|
+
console.log(' npx aihezu usage --json # JSON 格式输出所有服务');
|
|
386
|
+
console.log(' npx aihezu usage cc --json # JSON 格式输出 Claude Code');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function usageCommand(args = []) {
|
|
390
|
+
const wantsHelp = args.includes('help') || args.includes('--help') || args.includes('-h');
|
|
391
|
+
if (wantsHelp) {
|
|
392
|
+
showUsageHelp();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const outputJson = args.includes('--json');
|
|
397
|
+
|
|
398
|
+
// 提取服务过滤参数(第一个非选项参数)
|
|
399
|
+
const serviceFilter = args.find(arg => !arg.startsWith('-'));
|
|
400
|
+
|
|
401
|
+
// 服务别名映射
|
|
402
|
+
const serviceAliases = {
|
|
403
|
+
'cc': 'Claude Code',
|
|
404
|
+
'claude': 'Claude Code',
|
|
405
|
+
'codex': 'Codex',
|
|
406
|
+
'gemini': 'Google Gemini'
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
let filterServiceName = null;
|
|
410
|
+
if (serviceFilter) {
|
|
411
|
+
const normalizedFilter = serviceFilter.toLowerCase();
|
|
412
|
+
filterServiceName = serviceAliases[normalizedFilter];
|
|
413
|
+
|
|
414
|
+
if (!filterServiceName) {
|
|
415
|
+
console.error(`[错误] 未知的服务: ${serviceFilter}`);
|
|
416
|
+
console.error('');
|
|
417
|
+
console.error('可用服务: cc, claude, codex, gemini');
|
|
418
|
+
console.error('');
|
|
419
|
+
console.error('使用 "aihezu usage --help" 查看帮助');
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 收集所有服务的配置
|
|
425
|
+
const allServices = [
|
|
426
|
+
{ name: 'Claude Code', configs: readClaudeConfig() },
|
|
427
|
+
{ name: 'Codex', configs: readCodexConfig() },
|
|
428
|
+
{ name: 'Google Gemini', configs: readGeminiConfig() }
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
// 根据过滤参数筛选服务
|
|
432
|
+
const services = filterServiceName
|
|
433
|
+
? allServices.filter(s => s.name === filterServiceName)
|
|
434
|
+
: allServices;
|
|
435
|
+
|
|
436
|
+
let hasAnyConfig = false;
|
|
437
|
+
const allResults = {};
|
|
438
|
+
|
|
439
|
+
for (const service of services) {
|
|
440
|
+
if (service.configs.length === 0) continue;
|
|
441
|
+
|
|
442
|
+
hasAnyConfig = true;
|
|
443
|
+
console.log('');
|
|
444
|
+
console.log(`${'='.repeat(50)}`);
|
|
445
|
+
console.log(`=== ${service.name} 用量统计 ===`);
|
|
446
|
+
console.log(`${'='.repeat(50)}`);
|
|
447
|
+
console.log('');
|
|
448
|
+
|
|
449
|
+
allResults[service.name] = [];
|
|
450
|
+
|
|
451
|
+
for (const config of service.configs) {
|
|
452
|
+
console.log(`[查询中] ${config.source} - Token: ${maskToken(config.authToken)}`);
|
|
453
|
+
|
|
454
|
+
const result = await queryUsage(config.baseUrl, config.authToken);
|
|
455
|
+
|
|
456
|
+
if (result.error) {
|
|
457
|
+
console.log(`[错误] ${result.error}`);
|
|
458
|
+
console.log('');
|
|
459
|
+
allResults[service.name].push({
|
|
460
|
+
source: config.source,
|
|
461
|
+
error: result.error
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
if (!outputJson) {
|
|
465
|
+
displayUsageStats(result.stats, result.origin, config.source);
|
|
466
|
+
}
|
|
467
|
+
allResults[service.name].push({
|
|
468
|
+
source: config.source,
|
|
469
|
+
origin: result.origin,
|
|
470
|
+
stats: result.stats
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!hasAnyConfig) {
|
|
477
|
+
console.log('');
|
|
478
|
+
if (filterServiceName) {
|
|
479
|
+
console.log(`[提示] ${filterServiceName} 未配置。`);
|
|
480
|
+
} else {
|
|
481
|
+
console.log('[提示] 未找到任何已配置的服务。');
|
|
482
|
+
}
|
|
483
|
+
console.log('');
|
|
484
|
+
console.log('请先运行以下命令进行配置:');
|
|
485
|
+
console.log(' npx aihezu install claude # 配置 Claude Code');
|
|
486
|
+
console.log(' npx aihezu install codex # 配置 Codex');
|
|
487
|
+
console.log(' npx aihezu install gemini # 配置 Google Gemini');
|
|
488
|
+
console.log('');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (outputJson) {
|
|
493
|
+
console.log(JSON.stringify(allResults, null, 2));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = usageCommand;
|
package/package.json
CHANGED
package/commands/ccusage.js
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const https = require('https');
|
|
6
|
-
|
|
7
|
-
function normalizeHttpUrl(url) {
|
|
8
|
-
if (!url) return '';
|
|
9
|
-
const trimmed = String(url).trim();
|
|
10
|
-
if (!trimmed) return '';
|
|
11
|
-
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
12
|
-
return `https://${trimmed}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readClaudeEnvFromSettingsFile() {
|
|
16
|
-
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
17
|
-
if (!fs.existsSync(settingsPath)) {
|
|
18
|
-
return { env: null, settingsPath, error: null };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
23
|
-
const settings = JSON.parse(content);
|
|
24
|
-
const env = settings && settings.env ? settings.env : null;
|
|
25
|
-
return { env, settingsPath, error: null };
|
|
26
|
-
} catch (error) {
|
|
27
|
-
return { env: null, settingsPath, error };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function postJson(urlString, body, options = {}) {
|
|
32
|
-
const { timeoutMs = 15000 } = options;
|
|
33
|
-
const url = new URL(urlString);
|
|
34
|
-
const payload = JSON.stringify(body ?? {});
|
|
35
|
-
|
|
36
|
-
const transport = url.protocol === 'https:' ? https : http;
|
|
37
|
-
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
const req = transport.request(
|
|
40
|
-
{
|
|
41
|
-
method: 'POST',
|
|
42
|
-
hostname: url.hostname,
|
|
43
|
-
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
44
|
-
path: `${url.pathname}${url.search || ''}`,
|
|
45
|
-
headers: {
|
|
46
|
-
'Content-Type': 'application/json',
|
|
47
|
-
'Accept': 'application/json',
|
|
48
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
49
|
-
'User-Agent': 'aihezu-cli'
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
res => {
|
|
53
|
-
res.setEncoding('utf8');
|
|
54
|
-
let raw = '';
|
|
55
|
-
|
|
56
|
-
res.on('data', chunk => {
|
|
57
|
-
raw += chunk;
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
res.on('end', () => {
|
|
61
|
-
let json = null;
|
|
62
|
-
try {
|
|
63
|
-
json = raw ? JSON.parse(raw) : null;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// keep json as null, return raw for debugging
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
resolve({
|
|
69
|
-
statusCode: res.statusCode || 0,
|
|
70
|
-
raw,
|
|
71
|
-
json
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
req.on('error', reject);
|
|
78
|
-
req.setTimeout(timeoutMs, () => {
|
|
79
|
-
req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
req.write(payload);
|
|
83
|
-
req.end();
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function asNumber(value) {
|
|
88
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
89
|
-
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
90
|
-
return Number(value);
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function formatCost(value) {
|
|
96
|
-
const n = asNumber(value);
|
|
97
|
-
if (n === null) return '-';
|
|
98
|
-
return `$${n.toFixed(2)}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function formatPercent(current, limit) {
|
|
102
|
-
const c = asNumber(current);
|
|
103
|
-
const l = asNumber(limit);
|
|
104
|
-
if (c === null || l === null || l <= 0) return '-';
|
|
105
|
-
return `${((c / l) * 100).toFixed(1)}%`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function renderBar(current, limit, width = 18) {
|
|
109
|
-
const c = asNumber(current);
|
|
110
|
-
const l = asNumber(limit);
|
|
111
|
-
if (c === null || l === null || l <= 0) return '';
|
|
112
|
-
|
|
113
|
-
const ratio = Math.max(0, Math.min(1, c / l));
|
|
114
|
-
let filled = Math.round(ratio * width);
|
|
115
|
-
if (ratio > 0 && filled === 0) filled = 1;
|
|
116
|
-
const empty = Math.max(0, width - filled);
|
|
117
|
-
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function usageHint(current, limit) {
|
|
121
|
-
const c = asNumber(current);
|
|
122
|
-
const l = asNumber(limit);
|
|
123
|
-
if (c === null || l === null || l <= 0) return '';
|
|
124
|
-
const ratio = c / l;
|
|
125
|
-
if (ratio >= 1) return '[!] 已超出限制';
|
|
126
|
-
if (ratio >= 0.9) return '[!] 接近上限';
|
|
127
|
-
return '';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function showUsageHelp() {
|
|
131
|
-
console.log('用法: aihezu ccusage [--json]');
|
|
132
|
-
console.log('');
|
|
133
|
-
console.log('说明:');
|
|
134
|
-
console.log(' - 读取当前 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN(优先环境变量,其次 ~/.claude/settings.json)');
|
|
135
|
-
console.log(' - 自动请求 /apiStats/api/get-key-id 和 /apiStats/api/user-stats 并美观输出用量');
|
|
136
|
-
console.log('');
|
|
137
|
-
console.log('示例:');
|
|
138
|
-
console.log(' npx aihezu ccusage');
|
|
139
|
-
console.log(' npx aihezu ccusage --json');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function ccusageCommand(args = []) {
|
|
143
|
-
const wantsHelp = args.includes('help') || args.includes('--help') || args.includes('-h');
|
|
144
|
-
if (wantsHelp) {
|
|
145
|
-
showUsageHelp();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const outputJson = args.includes('--json');
|
|
150
|
-
|
|
151
|
-
// 读取环境变量
|
|
152
|
-
const envBaseUrl = process.env.ANTHROPIC_BASE_URL || '';
|
|
153
|
-
const envAuthToken = process.env.ANTHROPIC_AUTH_TOKEN || '';
|
|
154
|
-
|
|
155
|
-
// 读取配置文件
|
|
156
|
-
const { env: fileEnv, settingsPath, error } = readClaudeEnvFromSettingsFile();
|
|
157
|
-
if (error) {
|
|
158
|
-
console.log(`[WARN] 读取配置文件失败: ${settingsPath}`);
|
|
159
|
-
console.log(` 错误: ${error.message}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const fileBaseUrl = fileEnv?.ANTHROPIC_BASE_URL || '';
|
|
163
|
-
const fileAuthToken = fileEnv?.ANTHROPIC_AUTH_TOKEN || '';
|
|
164
|
-
|
|
165
|
-
// 确定最终使用的配置(环境变量优先)
|
|
166
|
-
let baseUrl = envBaseUrl || fileBaseUrl;
|
|
167
|
-
let authToken = envAuthToken || fileAuthToken;
|
|
168
|
-
|
|
169
|
-
baseUrl = normalizeHttpUrl(baseUrl);
|
|
170
|
-
authToken = String(authToken || '').trim();
|
|
171
|
-
|
|
172
|
-
if (!baseUrl || !authToken) {
|
|
173
|
-
console.error('[ERROR] 未找到 ANTHROPIC_BASE_URL 或 ANTHROPIC_AUTH_TOKEN。');
|
|
174
|
-
console.error(' 请先运行: npx aihezu install claude');
|
|
175
|
-
console.error(' 或设置环境变量: ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN');
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
let origin = '';
|
|
180
|
-
try {
|
|
181
|
-
origin = new URL(baseUrl).origin;
|
|
182
|
-
} catch (error) {
|
|
183
|
-
console.error('[ERROR] ANTHROPIC_BASE_URL 不是合法的 URL: ' + baseUrl);
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const keyIdUrl = `${origin}/apiStats/api/get-key-id`;
|
|
188
|
-
const statsUrl = `${origin}/apiStats/api/user-stats`;
|
|
189
|
-
|
|
190
|
-
console.log('');
|
|
191
|
-
console.log('=== Claude Code 用量统计 ===');
|
|
192
|
-
console.log(`域名: ${origin}`);
|
|
193
|
-
console.log('');
|
|
194
|
-
|
|
195
|
-
// 展示配置来源详情
|
|
196
|
-
console.log('=== 配置来源 ===');
|
|
197
|
-
|
|
198
|
-
// 环境变量
|
|
199
|
-
console.log('环境变量:');
|
|
200
|
-
console.log(` ANTHROPIC_BASE_URL: ${envBaseUrl || '(未设置)'}`);
|
|
201
|
-
console.log(` ANTHROPIC_AUTH_TOKEN: ${envAuthToken ? '***' + envAuthToken.slice(-8) : '(未设置)'}`);
|
|
202
|
-
|
|
203
|
-
// 配置文件
|
|
204
|
-
console.log('配置文件 (~/.claude/settings.json):');
|
|
205
|
-
if (error) {
|
|
206
|
-
console.log(` 状态: 读取失败 - ${error.message}`);
|
|
207
|
-
} else if (!fileEnv) {
|
|
208
|
-
console.log(` 状态: 文件不存在或无 env 字段`);
|
|
209
|
-
} else {
|
|
210
|
-
console.log(` ANTHROPIC_BASE_URL: ${fileBaseUrl || '(未设置)'}`);
|
|
211
|
-
console.log(` ANTHROPIC_AUTH_TOKEN: ${fileAuthToken ? '***' + fileAuthToken.slice(-8) : '(未设置)'}`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 最终使用的配置
|
|
215
|
-
console.log('最终使用:');
|
|
216
|
-
console.log(` BASE_URL: ${baseUrl} ${envBaseUrl ? '[环境变量]' : '[配置文件]'}`);
|
|
217
|
-
console.log(` AUTH_TOKEN: ***${authToken.slice(-8)} ${envAuthToken ? '[环境变量]' : '[配置文件]'}`);
|
|
218
|
-
console.log('');
|
|
219
|
-
|
|
220
|
-
const keyIdRes = await postJson(keyIdUrl, { apiKey: authToken });
|
|
221
|
-
if (keyIdRes.statusCode < 200 || keyIdRes.statusCode >= 300) {
|
|
222
|
-
console.error(`[ERROR] 获取 API Key ID 失败 (HTTP ${keyIdRes.statusCode})`);
|
|
223
|
-
if (keyIdRes.raw) console.error(keyIdRes.raw);
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const apiId =
|
|
228
|
-
keyIdRes?.json?.data?.id ||
|
|
229
|
-
keyIdRes?.json?.id ||
|
|
230
|
-
null;
|
|
231
|
-
|
|
232
|
-
if (!apiId) {
|
|
233
|
-
console.error('[ERROR] 返回值中未找到 API Key ID。');
|
|
234
|
-
if (keyIdRes.raw) console.error(keyIdRes.raw);
|
|
235
|
-
process.exit(1);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const statsRes = await postJson(statsUrl, { apiId });
|
|
239
|
-
if (statsRes.statusCode < 200 || statsRes.statusCode >= 300) {
|
|
240
|
-
console.error(`[ERROR] 获取用量失败 (HTTP ${statsRes.statusCode})`);
|
|
241
|
-
if (statsRes.raw) console.error(statsRes.raw);
|
|
242
|
-
process.exit(1);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!statsRes.json || typeof statsRes.json !== 'object') {
|
|
246
|
-
console.error('[ERROR] 获取用量失败:返回内容不是合法的 JSON。');
|
|
247
|
-
if (statsRes.raw) console.error(statsRes.raw);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const stats = (statsRes && statsRes.json && statsRes.json.data) ? statsRes.json.data : (statsRes.json || {});
|
|
252
|
-
|
|
253
|
-
if (outputJson) {
|
|
254
|
-
console.log(JSON.stringify(stats, null, 2));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const limits = (stats && typeof stats === 'object' && stats.limits && typeof stats.limits === 'object')
|
|
259
|
-
? stats.limits
|
|
260
|
-
: {};
|
|
261
|
-
|
|
262
|
-
const currentDailyCost = (stats.currentDailyCost !== undefined) ? stats.currentDailyCost : limits.currentDailyCost;
|
|
263
|
-
const dailyCostLimit = (stats.dailyCostLimit !== undefined) ? stats.dailyCostLimit : limits.dailyCostLimit;
|
|
264
|
-
const currentWindowCost = (stats.currentWindowCost !== undefined) ? stats.currentWindowCost : limits.currentWindowCost;
|
|
265
|
-
const rateLimitCost = (stats.rateLimitCost !== undefined) ? stats.rateLimitCost : limits.rateLimitCost;
|
|
266
|
-
|
|
267
|
-
const dailyRemaining = (() => {
|
|
268
|
-
const c = asNumber(currentDailyCost);
|
|
269
|
-
const l = asNumber(dailyCostLimit);
|
|
270
|
-
if (c === null || l === null) return null;
|
|
271
|
-
return l - c;
|
|
272
|
-
})();
|
|
273
|
-
|
|
274
|
-
const windowRemaining = (() => {
|
|
275
|
-
const c = asNumber(currentWindowCost);
|
|
276
|
-
const l = asNumber(rateLimitCost);
|
|
277
|
-
if (c === null || l === null) return null;
|
|
278
|
-
return l - c;
|
|
279
|
-
})();
|
|
280
|
-
|
|
281
|
-
console.log('=== 关键指标 ===');
|
|
282
|
-
console.log(
|
|
283
|
-
`日额度: ${formatCost(currentDailyCost)} / ${formatCost(dailyCostLimit)} (${formatPercent(currentDailyCost, dailyCostLimit)}) ` +
|
|
284
|
-
`${renderBar(currentDailyCost, dailyCostLimit)} ${usageHint(currentDailyCost, dailyCostLimit)}`.trimEnd()
|
|
285
|
-
);
|
|
286
|
-
if (dailyRemaining !== null) {
|
|
287
|
-
console.log(`日剩余: ${formatCost(dailyRemaining)}`);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
console.log('');
|
|
291
|
-
console.log(
|
|
292
|
-
`窗口: ${formatCost(currentWindowCost)} / ${formatCost(rateLimitCost)} (${formatPercent(currentWindowCost, rateLimitCost)}) ` +
|
|
293
|
-
`${renderBar(currentWindowCost, rateLimitCost)} ${usageHint(currentWindowCost, rateLimitCost)}`.trimEnd()
|
|
294
|
-
);
|
|
295
|
-
if (windowRemaining !== null) {
|
|
296
|
-
console.log(`窗口剩余: ${formatCost(windowRemaining)}`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
console.log('');
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
module.exports = ccusageCommand;
|