aihezu 2.1.0 → 2.3.1

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
  }
@@ -38,73 +41,97 @@ async function askQuestion(rl, question) {
38
41
  }
39
42
 
40
43
  async function main() {
41
- // Removed global rl creation
42
-
43
44
  try {
44
- if (args.length === 0 || args[0] === 'help') { // Handle 'aihezu' or 'aihezu help'
45
+ if (args.length === 0 || args[0] === 'help') {
45
46
  showHelp();
46
47
  return;
47
48
  }
48
49
 
49
50
  const commandName = args[0];
50
- let serviceName = args[1];
51
-
52
51
  if (!commands[commandName]) {
53
- console.error(`❌ 未知命��: ${commandName}`);
52
+ console.error(`❌ 未知命令: ${commandName}`);
54
53
  showHelp();
55
54
  process.exit(1);
56
55
  }
57
56
 
57
+ // Commands that don't require a service
58
+ if (commandName === 'ccusage') {
59
+ await commands[commandName](args.slice(1));
60
+ return;
61
+ }
62
+
63
+ let serviceName = '';
64
+ let commandArgs = [];
65
+
66
+ // Check if the second argument is a service name or an option/flag
67
+ if (args.length > 1 && !args[1].startsWith('-')) {
68
+ serviceName = args[1].toLowerCase();
69
+ commandArgs = args.slice(2);
70
+ } else {
71
+ // No service name provided, or an option was provided instead.
72
+ // The command arguments will be all arguments after the command itself.
73
+ commandArgs = args.slice(1);
74
+ }
75
+
76
+ // If service name is not provided, and command is 'install', enter interactive mode.
58
77
  if (!serviceName) {
59
78
  if (commandName === 'install') {
60
- // Init rl ONLY here for selection
61
79
  const rl = readline.createInterface({
62
80
  input: process.stdin,
63
81
  output: process.stdout
64
82
  });
65
83
 
66
84
  try {
67
- // Interactive service selection for 'install' command
68
85
  console.log('\n请选择要安装的服务:');
69
86
  const serviceKeys = Object.keys(services);
87
+ const claudeIndex = serviceKeys.indexOf('claude');
88
+
70
89
  serviceKeys.forEach((key, index) => {
71
- console.log(` [${index + 1}] ${services[key].displayName}`);
90
+ const isDefault = (key === 'claude');
91
+ console.log(` [${index + 1}] ${services[key].displayName}${isDefault ? ' (默认)' : ''}`);
72
92
  });
73
93
 
74
- let chosenIndex;
94
+ let chosenIndex = claudeIndex !== -1 ? claudeIndex : 0; // Default to Claude
75
95
  let validChoice = false;
96
+
76
97
  while (!validChoice) {
77
- const answer = await askQuestion(rl, '请输入选项编号: ');
78
- chosenIndex = parseInt(answer, 10) - 1;
79
- if (chosenIndex >= 0 && chosenIndex < serviceKeys.length) {
80
- serviceName = serviceKeys[chosenIndex];
81
- validChoice = true;
98
+ const answer = await askQuestion(rl, '请输入选项编号 (直接回车使用默认值): ');
99
+ if (answer === '') {
100
+ validChoice = true; // Use the default index
82
101
  } else {
83
- console.log('无效的选择,请输入列表中的数字。');
102
+ const selectedIdx = parseInt(answer, 10) - 1;
103
+ if (selectedIdx >= 0 && selectedIdx < serviceKeys.length) {
104
+ chosenIndex = selectedIdx;
105
+ validChoice = true;
106
+ } else {
107
+ console.log('无效的选择,请输入列表中的数字。');
108
+ }
84
109
  }
85
110
  }
111
+ serviceName = serviceKeys[chosenIndex];
86
112
  console.log(`✅ 已选择: ${services[serviceName].displayName}`);
113
+
87
114
  } finally {
88
- // Close rl immediately after selection and BEFORE calling the command
89
115
  rl.close();
90
116
  }
91
117
  } else {
92
- // For 'clear' or other commands, service name is mandatory
118
+ // For other commands like 'clear', service name is mandatory.
93
119
  console.error(`❌ 请为 '${commandName}' 命令指定一个服务名称。`);
94
120
  showHelp();
95
121
  process.exit(1);
96
122
  }
97
123
  }
98
124
 
99
- const service = services[serviceName.toLowerCase()];
125
+ const service = services[serviceName];
100
126
  if (!service) {
101
127
  console.error(`❌ 未知服务: ${serviceName}`);
102
128
  console.log('可用服务: ' + Object.keys(services).map(key => services[key].displayName).join(', '));
103
129
  process.exit(1);
104
130
  }
105
131
 
106
- // Execute
107
- await commands[commandName](service, args.slice(2));
132
+ // Execute the command with the correct service and arguments
133
+ await commands[commandName](service, commandArgs);
134
+
108
135
  } catch (error) {
109
136
  console.error('❌ 发生错误:', error);
110
137
  process.exit(1);
@@ -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.1.0",
3
+ "version": "2.3.1",
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
  },