aihezu 2.6.4 → 2.7.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 CHANGED
@@ -3,9 +3,12 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
+ const { askQuestion } = require('../lib/prompts');
6
7
 
7
8
  const commands = {
8
9
  install: require('../commands/install'),
10
+ config: require('../commands/config'),
11
+ check: require('../commands/check'),
9
12
  clear: require('../commands/clear'),
10
13
  usage: require('../commands/usage'),
11
14
  backup: require('../commands/backup'),
@@ -25,6 +28,8 @@ function showHelp() {
25
28
  console.log('用法: aihezu <command> [service]');
26
29
  console.log('\n命令:');
27
30
  console.log(' install <service> 配置服务 (API Key, URL, 网络设置)');
31
+ console.log(' config <service> 只修改配置 (不清理缓存, 不修改 hosts)');
32
+ console.log(' check [service] 检查当前配置状态 (配置文件 + 环境变量)');
28
33
  console.log(' clear <service> 清理缓存并刷新网络设置 (自动备份)');
29
34
  console.log(' backup <service> 手动备份配置');
30
35
  console.log(' recover <service> 从备份恢复配置');
@@ -53,6 +58,9 @@ function showHelp() {
53
58
  console.log(' 环境变量: GEMINI_API_KEY, GOOGLE_GEMINI_BASE_URL');
54
59
  console.log('\n示例:');
55
60
  console.log(' aihezu install claude');
61
+ console.log(' aihezu config claude (只修改配置,不清理缓存和 hosts)');
62
+ console.log(' aihezu check (检查所有服务的配置状态)');
63
+ console.log(' aihezu check claude (只检查 Claude Code 配置)');
56
64
  console.log(' aihezu clear codex');
57
65
  console.log(' aihezu backup claude (备份 Claude Code 配置)');
58
66
  console.log(' aihezu recover claude (恢复 Claude Code 配置)');
@@ -63,10 +71,6 @@ function showHelp() {
63
71
  console.log(' aihezu help (显示帮助信息)');
64
72
  }
65
73
 
66
- async function askQuestion(rl, question) {
67
- return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
68
- }
69
-
70
74
  async function main() {
71
75
  try {
72
76
  if (args.length === 0 || args[0] === 'help') {
@@ -82,7 +86,7 @@ async function main() {
82
86
  }
83
87
 
84
88
  // Commands that don't require a service
85
- if (commandName === 'usage') {
89
+ if (commandName === 'usage' || commandName === 'check') {
86
90
  await commands[commandName](args.slice(1));
87
91
  return;
88
92
  }
@@ -1,4 +1,5 @@
1
1
  const { createBackup, cleanOldBackups } = require('../lib/backup');
2
+ const { BACKUP_KEEP_COUNT } = require('../lib/constants');
2
3
 
3
4
  async function backupCommand(service) {
4
5
  console.log('');
@@ -30,11 +31,11 @@ async function backupCommand(service) {
30
31
  return;
31
32
  }
32
33
 
33
- // Clean old backups (keep 3 most recent)
34
+ // Clean old backups (keep most recent)
34
35
  console.log('');
35
36
  const deletedCount = cleanOldBackups({
36
37
  targetDir: targetDir,
37
- keepCount: 3
38
+ keepCount: BACKUP_KEEP_COUNT
38
39
  });
39
40
 
40
41
  if (deletedCount > 0) {
@@ -0,0 +1,237 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs');
4
+
5
+ const homeDir = os.homedir();
6
+
7
+ // Helper function to mask sensitive data
8
+ function maskSensitive(value) {
9
+ if (!value || value.length <= 10) {
10
+ return value ? value.substring(0, 3) + '...' : '(未设置)';
11
+ }
12
+ return value.substring(0, 8) + '...' + value.substring(value.length - 4);
13
+ }
14
+
15
+ // Check Claude Code configuration
16
+ function checkClaude() {
17
+ const configDir = path.join(homeDir, '.claude');
18
+ const settingsPath = path.join(configDir, 'settings.json');
19
+
20
+ console.log('\n📦 Claude Code 配置检查');
21
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
22
+
23
+ // Check config file
24
+ console.log('\n1️⃣ 配置文件: ~/.claude/settings.json');
25
+ if (fs.existsSync(settingsPath)) {
26
+ try {
27
+ const content = fs.readFileSync(settingsPath, 'utf8');
28
+ const settings = JSON.parse(content);
29
+
30
+ if (settings.env) {
31
+ console.log(' ✅ 文件存在');
32
+ console.log(' 📝 ANTHROPIC_AUTH_TOKEN:', maskSensitive(settings.env.ANTHROPIC_AUTH_TOKEN));
33
+ console.log(' 📝 ANTHROPIC_BASE_URL:', settings.env.ANTHROPIC_BASE_URL || '(未设置)');
34
+ } else {
35
+ console.log(' ⚠️ 文件存在但未包含 env 配置');
36
+ }
37
+ } catch (e) {
38
+ console.log(' ❌ 文件存在但格式错误:', e.message);
39
+ }
40
+ } else {
41
+ console.log(' ❌ 文件不存在');
42
+ }
43
+
44
+ // Check environment variables
45
+ console.log('\n2️⃣ 环境变量:');
46
+ const envToken = process.env.ANTHROPIC_AUTH_TOKEN;
47
+ const envUrl = process.env.ANTHROPIC_BASE_URL;
48
+ console.log(' 📝 ANTHROPIC_AUTH_TOKEN:', maskSensitive(envToken));
49
+ console.log(' 📝 ANTHROPIC_BASE_URL:', envUrl || '(未设置)');
50
+
51
+ // Priority explanation
52
+ console.log('\n3️⃣ 优先级说明:');
53
+ console.log(' 🔹 Claude Code 优先使用配置文件 (~/.claude/settings.json)');
54
+ console.log(' 🔹 如果配置文件不存在,才会读取环境变量');
55
+
56
+ // Active configuration
57
+ console.log('\n4️⃣ 当前生效的配置:');
58
+ if (fs.existsSync(settingsPath)) {
59
+ try {
60
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
61
+ if (settings.env) {
62
+ console.log(' ✅ 使用配置文件中的设置');
63
+ console.log(' 📍 API Key:', maskSensitive(settings.env.ANTHROPIC_AUTH_TOKEN));
64
+ console.log(' 📍 API URL:', settings.env.ANTHROPIC_BASE_URL || '(未设置)');
65
+ }
66
+ } catch (e) {
67
+ console.log(' ⚠️ 配置文件格式错误,可能使用环境变量');
68
+ }
69
+ } else {
70
+ console.log(' ℹ️ 配置文件不存在,使用环境变量');
71
+ console.log(' 📍 API Key:', maskSensitive(envToken));
72
+ console.log(' 📍 API URL:', envUrl || '(未设置)');
73
+ }
74
+ }
75
+
76
+ // Check Codex configuration
77
+ function checkCodex() {
78
+ const configDir = path.join(homeDir, '.codex');
79
+ const configPath = path.join(configDir, 'config.toml');
80
+ const authPath = path.join(configDir, 'auth.json');
81
+
82
+ console.log('\n📦 Codex 配置检查');
83
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
84
+
85
+ // Check config.toml
86
+ console.log('\n1️⃣ 配置文件: ~/.codex/config.toml');
87
+ if (fs.existsSync(configPath)) {
88
+ try {
89
+ const content = fs.readFileSync(configPath, 'utf8');
90
+ console.log(' ✅ 文件存在');
91
+
92
+ // Extract model
93
+ const modelMatch = content.match(/model\s*=\s*"([^"]+)"/);
94
+ if (modelMatch) {
95
+ console.log(' 📝 Model:', modelMatch[1]);
96
+ }
97
+
98
+ // Extract base_url
99
+ const baseUrlMatch = content.match(/base_url\s*=\s*"([^"]+)"/);
100
+ if (baseUrlMatch) {
101
+ console.log(' 📝 Base URL:', baseUrlMatch[1]);
102
+ }
103
+ } catch (e) {
104
+ console.log(' ❌ 文件存在但读取错误:', e.message);
105
+ }
106
+ } else {
107
+ console.log(' ❌ 文件不存在');
108
+ }
109
+
110
+ // Check auth.json
111
+ console.log('\n2️⃣ 认证文件: ~/.codex/auth.json');
112
+ if (fs.existsSync(authPath)) {
113
+ try {
114
+ const authData = JSON.parse(fs.readFileSync(authPath, 'utf8'));
115
+ console.log(' ✅ 文件存在');
116
+ console.log(' 📝 AIHEZU_OAI_KEY:', maskSensitive(authData.AIHEZU_OAI_KEY));
117
+ } catch (e) {
118
+ console.log(' ❌ 文件存在但格式错误:', e.message);
119
+ }
120
+ } else {
121
+ console.log(' ❌ 文件不存在');
122
+ }
123
+
124
+ // Check environment variables
125
+ console.log('\n3️⃣ 环境变量:');
126
+ const envKey = process.env.AIHEZU_OAI_KEY;
127
+ console.log(' 📝 AIHEZU_OAI_KEY:', maskSensitive(envKey));
128
+
129
+ // Priority explanation
130
+ console.log('\n4️⃣ 优先级说明:');
131
+ console.log(' 🔹 Codex 优先使用配置文件 (~/.codex/auth.json)');
132
+ console.log(' 🔹 如果配置文件不存在,才会读取环境变量');
133
+ }
134
+
135
+ // Check Gemini configuration
136
+ function checkGemini() {
137
+ const configDir = path.join(homeDir, '.gemini');
138
+ const envFilePath = path.join(configDir, '.env');
139
+ const settingsPath = path.join(configDir, 'settings.json');
140
+
141
+ console.log('\n📦 Google Gemini 配置检查');
142
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
143
+
144
+ // Check .env file
145
+ console.log('\n1️⃣ 配置文件: ~/.gemini/.env');
146
+ if (fs.existsSync(envFilePath)) {
147
+ try {
148
+ const content = fs.readFileSync(envFilePath, 'utf8');
149
+ console.log(' ✅ 文件存在');
150
+
151
+ const apiKeyMatch = content.match(/GEMINI_API_KEY="([^"]+)"/);
152
+ const baseUrlMatch = content.match(/GOOGLE_GEMINI_BASE_URL="([^"]+)"/);
153
+
154
+ console.log(' 📝 GEMINI_API_KEY:', maskSensitive(apiKeyMatch ? apiKeyMatch[1] : null));
155
+ console.log(' 📝 GOOGLE_GEMINI_BASE_URL:', baseUrlMatch ? baseUrlMatch[1] : '(未设置)');
156
+ } catch (e) {
157
+ console.log(' ❌ 文件存在但读取错误:', e.message);
158
+ }
159
+ } else {
160
+ console.log(' ❌ 文件不存在');
161
+ }
162
+
163
+ // Check settings.json (optional)
164
+ console.log('\n2️⃣ 可选配置: ~/.gemini/settings.json');
165
+ if (fs.existsSync(settingsPath)) {
166
+ console.log(' ✅ 文件存在');
167
+ } else {
168
+ console.log(' ℹ️ 文件不存在(可选)');
169
+ }
170
+
171
+ // Check environment variables
172
+ console.log('\n3️⃣ 环境变量:');
173
+ const envApiKey = process.env.GEMINI_API_KEY;
174
+ const envBaseUrl = process.env.GOOGLE_GEMINI_BASE_URL;
175
+ console.log(' 📝 GEMINI_API_KEY:', maskSensitive(envApiKey));
176
+ console.log(' 📝 GOOGLE_GEMINI_BASE_URL:', envBaseUrl || '(未设置)');
177
+
178
+ // Priority explanation
179
+ console.log('\n4️⃣ 优先级说明:');
180
+ console.log(' 🔹 Gemini 优先使用配置文件 (~/.gemini/.env)');
181
+ console.log(' 🔹 如果配置文件不存在,才会读取系统环境变量');
182
+ }
183
+
184
+ async function checkCommand(serviceOrArgs, args = []) {
185
+ console.log('');
186
+ console.log('🔍 配置检查工具');
187
+ console.log('🌐 Powered by https://aihezu.dev');
188
+ console.log('');
189
+
190
+ // Handle both calling patterns:
191
+ // 1. checkCommand(service, args) - when called with a specific service
192
+ // 2. checkCommand(args) - when called from main without a service
193
+ let service = null;
194
+ let actualArgs = args;
195
+
196
+ if (serviceOrArgs && typeof serviceOrArgs === 'object' && serviceOrArgs.name) {
197
+ // Called with a service object
198
+ service = serviceOrArgs;
199
+ } else if (Array.isArray(serviceOrArgs)) {
200
+ // Called with args array, need to parse for service name
201
+ actualArgs = serviceOrArgs;
202
+ const serviceName = actualArgs[0];
203
+
204
+ if (serviceName && !serviceName.startsWith('-')) {
205
+ // Load the service
206
+ const services = {
207
+ claude: require('../services/claude'),
208
+ codex: require('../services/codex'),
209
+ gemini: require('../services/gemini')
210
+ };
211
+ service = services[serviceName.toLowerCase()];
212
+ }
213
+ }
214
+
215
+ if (service) {
216
+ // Check specific service
217
+ if (service.name === 'claude') {
218
+ checkClaude();
219
+ } else if (service.name === 'codex') {
220
+ checkCodex();
221
+ } else if (service.name === 'gemini') {
222
+ checkGemini();
223
+ }
224
+ } else {
225
+ // Check all services
226
+ checkClaude();
227
+ checkCodex();
228
+ checkGemini();
229
+ }
230
+
231
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
232
+ console.log('💡 提示: 配置文件的优先级高于环境变量');
233
+ console.log('💡 使用 npx aihezu config <service> 可以快速修改配置');
234
+ console.log('');
235
+ }
236
+
237
+ module.exports = checkCommand;
package/commands/clear.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { modifyHostsFile } = require('../lib/hosts');
2
2
  const { cleanCache } = require('../lib/cache');
3
3
  const { createBackup, cleanOldBackups } = require('../lib/backup');
4
+ const { BACKUP_KEEP_COUNT } = require('../lib/constants');
4
5
 
5
6
  async function clearCommand(service) {
6
7
  console.log('');
@@ -37,10 +38,10 @@ async function clearCommand(service) {
37
38
  showHeader: true
38
39
  });
39
40
 
40
- // Clean old backups (keep 3 most recent)
41
+ // Clean old backups (keep most recent)
41
42
  cleanOldBackups({
42
43
  targetDir: service.cacheConfig.dir,
43
- keepCount: 3
44
+ keepCount: BACKUP_KEEP_COUNT
44
45
  });
45
46
  } else {
46
47
  console.log('ℹ️ 此服务未定义缓存配置。');
@@ -60,4 +61,4 @@ async function clearCommand(service) {
60
61
  console.log('✅ 清理完成!');
61
62
  }
62
63
 
63
- module.exports = clearCommand;
64
+ module.exports = clearCommand;
@@ -0,0 +1,155 @@
1
+ const readline = require('readline');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const { askQuestion } = require('../lib/prompts');
5
+
6
+ // Helper to extract value from args like --api=xxx or --api xxx
7
+ function getArgValue(args, keys) {
8
+ for (let i = 0; i < args.length; i++) {
9
+ const arg = args[i];
10
+
11
+ // Check for --key=value style
12
+ for (const key of keys) {
13
+ if (arg.startsWith(key + '=')) {
14
+ return arg.split('=')[1].trim();
15
+ }
16
+ // Check for --key value style
17
+ if (arg === key && i + 1 < args.length) {
18
+ return args[i + 1].trim();
19
+ }
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ async function configCommand(service, args = []) {
26
+ console.log('');
27
+ console.log('⚙️ ' + service.displayName + ' 配置修改工具');
28
+ console.log('🌐 Powered by https://aihezu.dev');
29
+ console.log('');
30
+ console.log('💡 提示: config 命令只修改配置,不清理缓存,不修改 hosts 文件');
31
+ console.log('');
32
+
33
+ const rl = readline.createInterface({
34
+ input: process.stdin,
35
+ output: process.stdout
36
+ });
37
+
38
+ try {
39
+ // Parse CLI args for defaults
40
+ const cliApiUrl = getArgValue(args, ['--api', '--url', '--base-url', '--host']);
41
+ const cliApiKey = getArgValue(args, ['--key', '--token', '--api-key']);
42
+ const cliModel = getArgValue(args, ['--model']);
43
+
44
+ // 1. API URL
45
+ const defaultUrl = cliApiUrl || service.defaultApiUrl;
46
+
47
+ if (cliApiUrl) {
48
+ console.log('💡 检测到命令行参数,默认 API 地址: ' + defaultUrl);
49
+ } else {
50
+ console.log('💡 默认 API 地址: ' + defaultUrl);
51
+ }
52
+
53
+ const apiUrlInput = await askQuestion(rl, '请输入 API 地址 (直接回车使用默认地址): ');
54
+ let apiUrl = apiUrlInput || defaultUrl;
55
+
56
+ // Append service-specific suffix if needed
57
+ const suffix = service.apiSuffix || '';
58
+ if (suffix) {
59
+ const normalizedSuffix = suffix.startsWith('/') ? suffix : `/${suffix}`;
60
+ const trimmedUrl = apiUrl.replace(/\/+$/, '');
61
+ if (!trimmedUrl.endsWith(normalizedSuffix)) {
62
+ apiUrl = trimmedUrl + normalizedSuffix;
63
+ } else {
64
+ apiUrl = trimmedUrl;
65
+ }
66
+ }
67
+
68
+ // Basic URL validation/normalization
69
+ // This part should handle http/https prefixing.
70
+ if (!/^https?:\/\//i.test(apiUrl)) {
71
+ apiUrl = 'https://' + apiUrl;
72
+ }
73
+
74
+ // 2. API Key
75
+ let defaultApiKey = cliApiKey || '';
76
+ let apiKeyPrompt = '请输入您的 API Key: ';
77
+ if (defaultApiKey) {
78
+ console.log('💡 检测到命令行参数提供了 API Key');
79
+ apiKeyPrompt = '请输入您的 API Key (直接回车使用提供的 Key): ';
80
+ }
81
+
82
+ const apiKeyInput = await askQuestion(rl, apiKeyPrompt);
83
+ let apiKey = apiKeyInput || defaultApiKey;
84
+
85
+ if (!apiKey) {
86
+ console.error('❌ API Key 不能为空。');
87
+ process.exit(1);
88
+ }
89
+
90
+ // 3. Extra Options (Service specific)
91
+ const options = {};
92
+ if (service.name === 'codex') {
93
+ let defaultModel = cliModel || 'gpt-5-codex';
94
+
95
+ console.log('请选择模型:');
96
+ console.log(' [1] gpt-5-codex' + (defaultModel === 'gpt-5-codex' ? ' (默认)' : ''));
97
+ console.log(' [2] claude-sonnet-4.5' + (defaultModel === 'claude-sonnet-4.5' ? ' (默认)' : ''));
98
+ console.log(' [3] 自定义模型名称' + (!['gpt-5-codex', 'claude-sonnet-4.5'].includes(defaultModel) ? ' (默认: ' + defaultModel + ')' : ''));
99
+
100
+ const choice = await askQuestion(rl, '请选择 [1/2/3]: ');
101
+
102
+ if (choice === '2') {
103
+ options.modelName = 'claude-sonnet-4.5';
104
+ } else if (choice === '3') {
105
+ const customModel = await askQuestion(rl, '请输入自定义模型名称: ');
106
+ options.modelName = customModel || defaultModel;
107
+ } else if (choice === '1' || choice === '') {
108
+ // Default choice (Enter or '1')
109
+ if (choice === '') {
110
+ options.modelName = defaultModel;
111
+ } else {
112
+ options.modelName = 'gpt-5-codex';
113
+ }
114
+ }
115
+ }
116
+
117
+ // 4. Confirmation
118
+ console.log('');
119
+ console.log('=== 配置摘要 ===');
120
+ console.log('服务类型: ' + service.displayName);
121
+ console.log('API 地址: ' + apiUrl);
122
+ if (options.modelName) console.log('模型: ' + options.modelName);
123
+ console.log('API Key: ' + apiKey.substring(0, 5) + '...');
124
+
125
+ console.log('');
126
+ const confirm = await askQuestion(rl, '是否继续? (Y/n, 默认回车确认): ');
127
+ // If user enters 'n' or 'no', cancel. Otherwise, proceed (including empty string for default Y).
128
+ if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
129
+ console.log('已取消。');
130
+ process.exit(0);
131
+ }
132
+
133
+ // 5. Execution - Only setup config, no cache cleaning, no hosts modification
134
+ console.log('');
135
+ console.log('=== 开始执行配置修改 ===');
136
+
137
+ // Setup Config only
138
+ const configFiles = await service.setupConfig(apiKey, apiUrl, options);
139
+ console.log('✅ 配置已写入:');
140
+ configFiles.forEach(f => console.log(' - ' + f));
141
+
142
+ console.log('');
143
+ console.log('ℹ️ 提示: config 命令不清理缓存,不修改 hosts 文件');
144
+ console.log('ℹ️ 如需完整安装(清理缓存 + 修改 hosts),请使用 install 命令');
145
+ console.log('');
146
+ console.log('✅ 配置修改完成!');
147
+
148
+ } catch (error) {
149
+ console.error('❌ 配置修改过程中发生错误:', error);
150
+ } finally {
151
+ rl.close();
152
+ }
153
+ }
154
+
155
+ module.exports = configCommand;
@@ -3,10 +3,7 @@ const path = require('path');
3
3
  const fs = require('fs');
4
4
  const { modifyHostsFile } = require('../lib/hosts');
5
5
  const { cleanCache } = require('../lib/cache');
6
-
7
- function askQuestion(rl, question) {
8
- return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
9
- }
6
+ const { askQuestion } = require('../lib/prompts');
10
7
 
11
8
  // Helper to extract value from args like --api=xxx or --api xxx
12
9
  function getArgValue(args, keys) {
@@ -56,19 +53,16 @@ async function installCommand(service, args = []) {
56
53
  const apiUrlInput = await askQuestion(rl, '请输入 API 地址 (直接回车使用默认地址): ');
57
54
  let apiUrl = apiUrlInput || defaultUrl;
58
55
 
59
- // Determine service-specific suffix
60
- let suffix = '';
61
- if (service.name === 'claude') {
62
- suffix = '/api';
63
- } else if (service.name === 'codex') {
64
- suffix = '/openai';
65
- } else if (service.name === 'gemini') {
66
- suffix = '/gemini';
67
- }
68
-
69
- // Append suffix if apiUrl does not already end with it
70
- if (suffix && !apiUrl.endsWith(suffix)) {
71
- apiUrl = apiUrl.replace(/\/+$/, '') + suffix; // Remove any trailing slash and add suffix
56
+ // Append service-specific suffix if needed
57
+ const suffix = service.apiSuffix || '';
58
+ if (suffix) {
59
+ const normalizedSuffix = suffix.startsWith('/') ? suffix : `/${suffix}`;
60
+ const trimmedUrl = apiUrl.replace(/\/+$/, '');
61
+ if (!trimmedUrl.endsWith(normalizedSuffix)) {
62
+ apiUrl = trimmedUrl + normalizedSuffix;
63
+ } else {
64
+ apiUrl = trimmedUrl;
65
+ }
72
66
  }
73
67
 
74
68
  // Basic URL validation/normalization
@@ -165,7 +159,7 @@ async function installCommand(service, args = []) {
165
159
  } else {
166
160
  console.log('');
167
161
  console.log('=== 网络配置 ===');
168
- console.log('���️ 此服务不需要修改 hosts 文件。');
162
+ console.log('ℹ️ 此服务不需要修改 hosts 文件。');
169
163
  }
170
164
 
171
165
  console.log('');
@@ -178,4 +172,4 @@ async function installCommand(service, args = []) {
178
172
  }
179
173
  }
180
174
 
181
- module.exports = installCommand;
175
+ module.exports = installCommand;
@@ -1,9 +1,6 @@
1
1
  const readline = require('readline');
2
2
  const { listBackups, restoreBackup, createBackup } = require('../lib/backup');
3
-
4
- async function askQuestion(rl, question) {
5
- return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
6
- }
3
+ const { askQuestion } = require('../lib/prompts');
7
4
 
8
5
  async function recoverCommand(service) {
9
6
  console.log('');
package/commands/reset.js CHANGED
@@ -2,10 +2,7 @@ const readline = require('readline');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { createBackup } = require('../lib/backup');
5
-
6
- async function askQuestion(rl, question) {
7
- return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
8
- }
5
+ const { askQuestion } = require('../lib/prompts');
9
6
 
10
7
  async function resetCommand(service) {
11
8
  console.log('');
package/commands/usage.js CHANGED
@@ -1,8 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
- const http = require('http');
5
- const https = require('https');
4
+ const { postJson } = require('../lib/http');
6
5
 
7
6
  function normalizeHttpUrl(url) {
8
7
  if (!url) return '';
@@ -249,62 +248,6 @@ function readGeminiConfig() {
249
248
  return configs;
250
249
  }
251
250
 
252
- function postJson(urlString, body, options = {}) {
253
- const { timeoutMs = 15000 } = options;
254
- const url = new URL(urlString);
255
- const payload = JSON.stringify(body ?? {});
256
-
257
- const transport = url.protocol === 'https:' ? https : http;
258
-
259
- return new Promise((resolve, reject) => {
260
- const req = transport.request(
261
- {
262
- method: 'POST',
263
- hostname: url.hostname,
264
- port: url.port || (url.protocol === 'https:' ? 443 : 80),
265
- path: `${url.pathname}${url.search || ''}`,
266
- headers: {
267
- 'Content-Type': 'application/json',
268
- 'Accept': 'application/json',
269
- 'Content-Length': Buffer.byteLength(payload),
270
- 'User-Agent': 'aihezu-cli'
271
- }
272
- },
273
- res => {
274
- res.setEncoding('utf8');
275
- let raw = '';
276
-
277
- res.on('data', chunk => {
278
- raw += chunk;
279
- });
280
-
281
- res.on('end', () => {
282
- let json = null;
283
- try {
284
- json = raw ? JSON.parse(raw) : null;
285
- } catch (error) {
286
- // keep json as null, return raw for debugging
287
- }
288
-
289
- resolve({
290
- statusCode: res.statusCode || 0,
291
- raw,
292
- json
293
- });
294
- });
295
- }
296
- );
297
-
298
- req.on('error', reject);
299
- req.setTimeout(timeoutMs, () => {
300
- req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
301
- });
302
-
303
- req.write(payload);
304
- req.end();
305
- });
306
- }
307
-
308
251
  function asNumber(value) {
309
252
  if (typeof value === 'number' && Number.isFinite(value)) return value;
310
253
  if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
@@ -613,12 +556,24 @@ async function usageCommand(args = []) {
613
556
  ? uniqueConfigsByOrigin(service.configs)
614
557
  : service.configs;
615
558
 
616
- for (const config of configsToUse) {
559
+ const requests = configsToUse.map(config => {
617
560
  const effectiveKey = overrideKey || config.authToken;
618
561
  const sourceLabel = overrideKey ? `命令行 Key (${config.source})` : config.source;
619
562
  console.log(`[查询中] ${sourceLabel} - Token: ${maskToken(effectiveKey)}`);
563
+ return {
564
+ sourceLabel,
565
+ promise: queryUsage(config.baseUrl, effectiveKey)
566
+ };
567
+ });
568
+
569
+ const results = await Promise.allSettled(requests.map(item => item.promise));
620
570
 
621
- const result = await queryUsage(config.baseUrl, effectiveKey);
571
+ for (let i = 0; i < results.length; i += 1) {
572
+ const outcome = results[i];
573
+ const sourceLabel = requests[i].sourceLabel;
574
+ const result = outcome.status === 'fulfilled'
575
+ ? outcome.value
576
+ : { error: outcome.reason ? outcome.reason.message : '查询失败' };
622
577
 
623
578
  if (result.error) {
624
579
  console.log(`[错误] ${result.error}`);
package/lib/backup.js CHANGED
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const { execSync } = require('child_process');
4
4
  const { getLocalTimestamp } = require('./cache');
5
+ const { BACKUP_KEEP_COUNT } = require('./constants');
5
6
 
6
7
  /**
7
8
  * Create a tar.gz backup of files/directories
@@ -186,11 +187,11 @@ function restoreBackup(options = {}) {
186
187
  * Clean old backups, keeping only the most recent N backups
187
188
  * @param {Object} options - Clean options
188
189
  * @param {string} options.targetDir - Directory containing backups
189
- * @param {number} options.keepCount - Number of recent backups to keep (default: 3)
190
+ * @param {number} options.keepCount - Number of recent backups to keep (default: BACKUP_KEEP_COUNT)
190
191
  * @returns {number} - Number of backups deleted
191
192
  */
192
193
  function cleanOldBackups(options = {}) {
193
- const { targetDir, keepCount = 3 } = options;
194
+ const { targetDir, keepCount = BACKUP_KEEP_COUNT } = options;
194
195
 
195
196
  if (!targetDir || !fs.existsSync(targetDir)) {
196
197
  return 0;
package/lib/cache.js CHANGED
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const os = require('os');
3
3
  const fs = require('fs');
4
4
  const { execSync } = require('child_process');
5
+ const { BACKUP_KEEP_COUNT } = require('./constants');
5
6
 
6
7
  // Generate local timestamp: YYYYMMDDHHMMSS
7
8
  function getLocalTimestamp() {
@@ -91,7 +92,7 @@ function cleanCache(options = {}) {
91
92
  }
92
93
  }
93
94
 
94
- // Clean old backups (keep only the 3 most recent backups)
95
+ // Clean old backups (keep only the most recent backups)
95
96
  try {
96
97
  const items = fs.readdirSync(targetDir);
97
98
  const backupItems = [];
@@ -129,15 +130,15 @@ function cleanCache(options = {}) {
129
130
  backupGroups[backup.baseName].push(backup);
130
131
  }
131
132
 
132
- // For each group, keep only the 3 most recent backups
133
+ // For each group, keep only the most recent backups
133
134
  for (const baseName in backupGroups) {
134
135
  const group = backupGroups[baseName];
135
136
 
136
137
  // Sort by timestamp descending (newest first)
137
138
  group.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
138
139
 
139
- // Delete backups beyond the 3 most recent
140
- const backupsToDelete = group.slice(3);
140
+ // Delete backups beyond the most recent
141
+ const backupsToDelete = group.slice(BACKUP_KEEP_COUNT);
141
142
 
142
143
  for (const backup of backupsToDelete) {
143
144
  try {
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ BACKUP_KEEP_COUNT: 3,
3
+ HTTP_TIMEOUT: 15000,
4
+ HTTP_RETRY_COUNT: 3,
5
+ HTTP_RETRY_DELAY: 1000,
6
+ DNS_FLUSH_TIMEOUT: 5000,
7
+ BACKUP_FILE_PREFIX: 'backup-',
8
+ HOSTS_BACKUP_PREFIX: 'hosts.backup-'
9
+ };
package/lib/hosts.js CHANGED
@@ -1,18 +1,7 @@
1
1
  const { execSync } = require('child_process');
2
2
  const os = require('os');
3
3
  const fs = require('fs');
4
-
5
- // Generate local timestamp: YYYYMMDDHHMMSS
6
- function getLocalTimestamp() {
7
- const now = new Date();
8
- const year = now.getFullYear();
9
- const month = String(now.getMonth() + 1).padStart(2, '0');
10
- const day = String(now.getDate()).padStart(2, '0');
11
- const hours = String(now.getHours()).padStart(2, '0');
12
- const minutes = String(now.getMinutes()).padStart(2, '0');
13
- const seconds = String(now.getSeconds()).padStart(2, '0');
14
- return `${year}${month}${day}${hours}${minutes}${seconds}`;
15
- }
4
+ const { getLocalTimestamp } = require('./cache');
16
5
 
17
6
  /**
18
7
  * Modifies the system hosts file.
@@ -72,7 +61,7 @@ function modifyHostsFile(hostEntries = [], markerText = '# Added by aihezu CLI')
72
61
 
73
62
  // Remove lines matching our target domains
74
63
  return !targetDomains.some(domain => {
75
- const regex = new RegExp('\\s+' + domain.replace('.', '\\.') + '(\s|$)', 'i');
64
+ const regex = new RegExp('\\s+' + domain.replace(/\./g, '\\.') + '(\\s|$)', 'i');
76
65
  return regex.test(trimmed);
77
66
  });
78
67
  });
package/lib/http.js ADDED
@@ -0,0 +1,101 @@
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const { HTTP_TIMEOUT, HTTP_RETRY_COUNT, HTTP_RETRY_DELAY } = require('./constants');
4
+
5
+ const DEFAULT_RETRY_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
6
+
7
+ function sleep(ms) {
8
+ return new Promise(resolve => setTimeout(resolve, ms));
9
+ }
10
+
11
+ function sendJsonRequest(method, urlString, body, timeoutMs) {
12
+ const url = new URL(urlString);
13
+ const payload = JSON.stringify(body ?? {});
14
+ const transport = url.protocol === 'https:' ? https : http;
15
+
16
+ return new Promise((resolve, reject) => {
17
+ const req = transport.request(
18
+ {
19
+ method,
20
+ hostname: url.hostname,
21
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
22
+ 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
+ }
29
+ },
30
+ res => {
31
+ res.setEncoding('utf8');
32
+ let raw = '';
33
+
34
+ res.on('data', chunk => {
35
+ raw += chunk;
36
+ });
37
+
38
+ res.on('end', () => {
39
+ let json = null;
40
+ try {
41
+ json = raw ? JSON.parse(raw) : null;
42
+ } catch (error) {
43
+ // ignore JSON parse errors
44
+ }
45
+
46
+ resolve({
47
+ statusCode: res.statusCode || 0,
48
+ raw,
49
+ json
50
+ });
51
+ });
52
+ }
53
+ );
54
+
55
+ req.on('error', reject);
56
+ req.setTimeout(timeoutMs, () => {
57
+ req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
58
+ });
59
+
60
+ req.write(payload);
61
+ req.end();
62
+ });
63
+ }
64
+
65
+ async function postJson(urlString, body, options = {}) {
66
+ const {
67
+ timeoutMs = HTTP_TIMEOUT,
68
+ retryCount = HTTP_RETRY_COUNT,
69
+ retryDelayMs = HTTP_RETRY_DELAY,
70
+ retryStatusCodes = DEFAULT_RETRY_STATUS_CODES
71
+ } = options;
72
+
73
+ const retryCodes = retryStatusCodes instanceof Set ? retryStatusCodes : new Set(retryStatusCodes);
74
+ let attempt = 0;
75
+ let lastError = null;
76
+
77
+ while (attempt <= retryCount) {
78
+ try {
79
+ const response = await sendJsonRequest('POST', urlString, body, timeoutMs);
80
+ if (retryCodes.has(response.statusCode) && attempt < retryCount) {
81
+ await sleep(retryDelayMs * Math.pow(2, attempt));
82
+ attempt += 1;
83
+ continue;
84
+ }
85
+ return response;
86
+ } catch (error) {
87
+ lastError = error;
88
+ if (attempt >= retryCount) {
89
+ break;
90
+ }
91
+ await sleep(retryDelayMs * Math.pow(2, attempt));
92
+ attempt += 1;
93
+ }
94
+ }
95
+
96
+ throw lastError || new Error('Request failed');
97
+ }
98
+
99
+ module.exports = {
100
+ postJson
101
+ };
package/lib/prompts.js ADDED
@@ -0,0 +1,7 @@
1
+ function askQuestion(rl, question) {
2
+ return new Promise(resolve => rl.question(question, answer => resolve(String(answer).trim())));
3
+ }
4
+
5
+ module.exports = {
6
+ askQuestion
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aihezu",
3
- "version": "2.6.4",
3
+ "version": "2.7.0",
4
4
  "description": "AI 开发环境配置工具 - 支持 Claude Code, Codex, Google Gemini 的本地化配置、代理设置与缓存清理",
5
5
  "main": "bin/aihezu.js",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  "author": "aihezu",
34
34
  "license": "MIT",
35
35
  "engines": {
36
- "node": ">=14.0.0"
36
+ "node": ">=14.14.0"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
@@ -10,7 +10,8 @@ const settingsPath = path.join(configDir, 'settings.json');
10
10
  module.exports = {
11
11
  name: 'claude',
12
12
  displayName: 'Claude Code',
13
- defaultApiUrl: 'https://cc.aihezu.dev/api',
13
+ defaultApiUrl: 'https://cn.aihezu.dev/api',
14
+ apiSuffix: '/api',
14
15
 
15
16
  // Cache cleaning configuration
16
17
  cacheConfig: {
package/services/codex.js CHANGED
@@ -12,6 +12,7 @@ module.exports = {
12
12
  name: 'codex',
13
13
  displayName: 'Codex',
14
14
  defaultApiUrl: 'https://cc.aihezu.dev/openai',
15
+ apiSuffix: '/openai',
15
16
 
16
17
  // Cache cleaning configuration (minimal for Codex usually)
17
18
  cacheConfig: {
@@ -12,6 +12,7 @@ module.exports = {
12
12
  name: 'gemini',
13
13
  displayName: 'Google Gemini',
14
14
  defaultApiUrl: 'https://cc.aihezu.dev/gemini',
15
+ apiSuffix: '/gemini',
15
16
 
16
17
  // Cache cleaning configuration
17
18
  cacheConfig: {