aihezu 2.2.0 → 2.3.2

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/README.md CHANGED
@@ -18,6 +18,7 @@
18
18
  - Claude:默认 `https://cc.aihezu.dev/api`,企业可用 `--api` / `--api-url` 指定独立域名
19
19
  - Codex:默认 `https://cc.aihezu.dev/openai`,企业可用 `--api` / `--api-url` 指定独立域名;写入 `~/.codex/config.toml` 和 `auth.json`,使用 `AIHEZU_OAI_KEY`
20
20
  - `ccclear`:清理 Claude Code 缓存和配置
21
+ - `ccusage`:查看 Claude Code API 用量统计(读取 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`)
21
22
 
22
23
  ## 清理内容
23
24
 
@@ -84,6 +85,14 @@ npm install -g aihezu
84
85
  sudo ccclear
85
86
  ```
86
87
 
88
+ ### 查看 Claude Code 用量(ccusage)
89
+
90
+ 无需管理员权限:
91
+
92
+ ```bash
93
+ npx aihezu ccusage
94
+ ```
95
+
87
96
  ## 运行效果
88
97
 
89
98
  ```
package/bin/aihezu.js CHANGED
@@ -6,7 +6,8 @@ const readline = require('readline');
6
6
 
7
7
  const commands = {
8
8
  install: require('../commands/install'),
9
- clear: require('../commands/clear')
9
+ clear: require('../commands/clear'),
10
+ ccusage: require('../commands/ccusage')
10
11
  };
11
12
 
12
13
  const services = {
@@ -22,6 +23,7 @@ function showHelp() {
22
23
  console.log('\n命令:');
23
24
  console.log(' install <service> 配置服务 (API Key, URL, 网络设置)');
24
25
  console.log(' clear <service> 清理缓存并刷新网络设置');
26
+ console.log(' ccusage 查看 Claude Code 用量统计');
25
27
  console.log('\n服务:');
26
28
  console.log(' claude Claude Code');
27
29
  console.log(' codex Codex');
@@ -29,6 +31,7 @@ function showHelp() {
29
31
  console.log('\n示例:');
30
32
  console.log(' aihezu install claude');
31
33
  console.log(' aihezu clear codex');
34
+ console.log(' aihezu ccusage');
32
35
  console.log(' aihezu install (交互式安装)');
33
36
  console.log(' aihezu help (显示帮助信息)');
34
37
  }
@@ -51,6 +54,12 @@ async function main() {
51
54
  process.exit(1);
52
55
  }
53
56
 
57
+ // Commands that don't require a service
58
+ if (commandName === 'ccusage') {
59
+ await commands[commandName](args.slice(1));
60
+ return;
61
+ }
62
+
54
63
  let serviceName = '';
55
64
  let commandArgs = [];
56
65
 
@@ -129,4 +138,4 @@ async function main() {
129
138
  }
130
139
  }
131
140
 
132
- main();
141
+ main();
@@ -0,0 +1,278 @@
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
+ let baseUrl = process.env.ANTHROPIC_BASE_URL || '';
152
+ let authToken = process.env.ANTHROPIC_AUTH_TOKEN || '';
153
+ let sourceHint = '环境变量';
154
+
155
+ if (!baseUrl || !authToken) {
156
+ const { env, settingsPath, error } = readClaudeEnvFromSettingsFile();
157
+ if (error) {
158
+ console.log(`⚠️ 读取配置文件失败: ${settingsPath}`);
159
+ console.log(` 错误: ${error.message}`);
160
+ }
161
+
162
+ if (env) {
163
+ baseUrl = baseUrl || env.ANTHROPIC_BASE_URL || '';
164
+ authToken = authToken || env.ANTHROPIC_AUTH_TOKEN || '';
165
+ sourceHint = `配置文件 (${settingsPath})`;
166
+ }
167
+ }
168
+
169
+ baseUrl = normalizeHttpUrl(baseUrl);
170
+ authToken = String(authToken || '').trim();
171
+
172
+ if (!baseUrl || !authToken) {
173
+ console.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('❌ 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(`🔎 配置来源: ${sourceHint}`);
194
+ console.log('');
195
+
196
+ const keyIdRes = await postJson(keyIdUrl, { apiKey: authToken });
197
+ if (keyIdRes.statusCode < 200 || keyIdRes.statusCode >= 300) {
198
+ console.error(`❌ 获取 API Key ID 失败 (HTTP ${keyIdRes.statusCode})`);
199
+ if (keyIdRes.raw) console.error(keyIdRes.raw);
200
+ process.exit(1);
201
+ }
202
+
203
+ const apiId =
204
+ keyIdRes?.json?.data?.id ||
205
+ keyIdRes?.json?.id ||
206
+ null;
207
+
208
+ if (!apiId) {
209
+ console.error('❌ 返回值中未找到 API Key ID。');
210
+ if (keyIdRes.raw) console.error(keyIdRes.raw);
211
+ process.exit(1);
212
+ }
213
+
214
+ const statsRes = await postJson(statsUrl, { apiId });
215
+ if (statsRes.statusCode < 200 || statsRes.statusCode >= 300) {
216
+ console.error(`❌ 获取用量失败 (HTTP ${statsRes.statusCode})`);
217
+ if (statsRes.raw) console.error(statsRes.raw);
218
+ process.exit(1);
219
+ }
220
+
221
+ if (!statsRes.json || typeof statsRes.json !== 'object') {
222
+ console.error('❌ 获取用量失败:返回内容不是合法的 JSON。');
223
+ if (statsRes.raw) console.error(statsRes.raw);
224
+ process.exit(1);
225
+ }
226
+
227
+ const stats = (statsRes && statsRes.json && statsRes.json.data) ? statsRes.json.data : (statsRes.json || {});
228
+
229
+ if (outputJson) {
230
+ console.log(JSON.stringify(stats, null, 2));
231
+ return;
232
+ }
233
+
234
+ const limits = (stats && typeof stats === 'object' && stats.limits && typeof stats.limits === 'object')
235
+ ? stats.limits
236
+ : {};
237
+
238
+ const currentDailyCost = (stats.currentDailyCost !== undefined) ? stats.currentDailyCost : limits.currentDailyCost;
239
+ const dailyCostLimit = (stats.dailyCostLimit !== undefined) ? stats.dailyCostLimit : limits.dailyCostLimit;
240
+ const currentWindowCost = (stats.currentWindowCost !== undefined) ? stats.currentWindowCost : limits.currentWindowCost;
241
+ const rateLimitCost = (stats.rateLimitCost !== undefined) ? stats.rateLimitCost : limits.rateLimitCost;
242
+
243
+ const dailyRemaining = (() => {
244
+ const c = asNumber(currentDailyCost);
245
+ const l = asNumber(dailyCostLimit);
246
+ if (c === null || l === null) return null;
247
+ return l - c;
248
+ })();
249
+
250
+ const windowRemaining = (() => {
251
+ const c = asNumber(currentWindowCost);
252
+ const l = asNumber(rateLimitCost);
253
+ if (c === null || l === null) return null;
254
+ return l - c;
255
+ })();
256
+
257
+ console.log('=== 关键指标 ===');
258
+ console.log(
259
+ `日额度: ${formatCost(currentDailyCost)} / ${formatCost(dailyCostLimit)} (${formatPercent(currentDailyCost, dailyCostLimit)}) ` +
260
+ `${renderBar(currentDailyCost, dailyCostLimit)} ${usageHint(currentDailyCost, dailyCostLimit)}`.trimEnd()
261
+ );
262
+ if (dailyRemaining !== null) {
263
+ console.log(`日剩余: ${formatCost(dailyRemaining)}`);
264
+ }
265
+
266
+ console.log('');
267
+ console.log(
268
+ `窗口: ${formatCost(currentWindowCost)} / ${formatCost(rateLimitCost)} (${formatPercent(currentWindowCost, rateLimitCost)}) ` +
269
+ `${renderBar(currentWindowCost, rateLimitCost)} ${usageHint(currentWindowCost, rateLimitCost)}`.trimEnd()
270
+ );
271
+ if (windowRemaining !== null) {
272
+ console.log(`窗口剩余: ${formatCost(windowRemaining)}`);
273
+ }
274
+
275
+ console.log('');
276
+ }
277
+
278
+ module.exports = ccusageCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aihezu",
3
- "version": "2.2.0",
3
+ "version": "2.3.2",
4
4
  "description": "AI 开发环境配置工具 - 支持 Claude Code, Codex, Google Gemini 的本地化配置、代理设置与缓存清理",
5
5
  "main": "bin/aihezu.js",
6
6
  "bin": {
@@ -8,6 +8,14 @@
8
8
  "ccclear": "bin/ccclear.js",
9
9
  "ccinstall": "bin/ccinstall.js"
10
10
  },
11
+ "files": [
12
+ "bin/",
13
+ "commands/",
14
+ "services/",
15
+ "lib/",
16
+ "README.md",
17
+ "INSTALL_USAGE.md"
18
+ ],
11
19
  "scripts": {
12
20
  "test": "node bin/aihezu.js help"
13
21
  },