easy-cc-api-switch 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bluemomo112
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Easy CC API Switch
2
+
3
+ Claude Code API 配置快速切换工具
4
+
5
+ 这是 `claude-config-switch` 的增强版本,在原有功能基础上新增了**快速循环切换**功能。
6
+
7
+ ## ✨ 新增功能
8
+
9
+ ### 快速循环切换
10
+ 使用 `ccs next` 或 `ccs n` 命令可以快速切换到下一个 API 配置,无需交互确认。
11
+
12
+ ```bash
13
+ # 切换到下一个配置
14
+ ccs next
15
+
16
+ # 或使用简写
17
+ ccs n
18
+ ```
19
+
20
+ 当切换到最后一个配置时,会自动循环回到第一个配置。
21
+
22
+ ## 📦 安装
23
+
24
+ ```bash
25
+ npm install -g easy-cc-api-switch
26
+ ```
27
+
28
+ ## 🚀 使用方法
29
+
30
+ ### 1. 配置 API 列表
31
+
32
+ 首先需要创建 API 配置文件:
33
+
34
+ ```bash
35
+ ccs o api
36
+ ```
37
+
38
+ 这会打开 `~/.claude/apiConfigs.json` 文件,按以下格式添加你的 API 配置:
39
+
40
+ ```json
41
+ [
42
+ {
43
+ "name": "官方API",
44
+ "config": {
45
+ "env": {
46
+ "ANTHROPIC_AUTH_TOKEN": "sk-ant-xxx",
47
+ "ANTHROPIC_BASE_URL": "https://api.anthropic.com",
48
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
49
+ },
50
+ "permissions": {
51
+ "allow": [],
52
+ "deny": []
53
+ }
54
+ }
55
+ },
56
+ {
57
+ "name": "代理API",
58
+ "config": {
59
+ "env": {
60
+ "ANTHROPIC_AUTH_TOKEN": "your-proxy-token",
61
+ "ANTHROPIC_BASE_URL": "https://your-proxy.com/api",
62
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
63
+ },
64
+ "permissions": {
65
+ "allow": [],
66
+ "deny": []
67
+ }
68
+ }
69
+ }
70
+ ]
71
+ ```
72
+
73
+ ### 2. 切换配置
74
+
75
+ #### 交互式选择(原有功能)
76
+ ```bash
77
+ ccs ls
78
+ # 或
79
+ ccs list
80
+ ```
81
+
82
+ 会显示所有可用配置,通过上下键或输入序号选择。
83
+
84
+ #### 快速循环切换(新功能)⭐
85
+ ```bash
86
+ ccs next
87
+ # 或
88
+ ccs n
89
+ ```
90
+
91
+ 直接切换到下一个配置,无需确认。适合频繁切换场景。
92
+
93
+ ### 3. 其他命令
94
+
95
+ ```bash
96
+ # 查看帮助
97
+ ccs --help
98
+
99
+ # 打开 API 配置文件
100
+ ccs o api
101
+
102
+ # 打开 settings 配置文件
103
+ ccs o setting
104
+
105
+ # 检查 API 健康状态
106
+ ccs health
107
+
108
+ # 配置企微通知
109
+ ccs notify
110
+
111
+ # 查看版本
112
+ ccs -v
113
+ ```
114
+
115
+ ## 📋 配置文件说明
116
+
117
+ 工具使用两个配置文件:
118
+
119
+ - `~/.claude/apiConfigs.json` - 存储所有可用的 API 配置列表
120
+ - `~/.claude/settings.json` - 当前激活的配置(由 Claude Code 使用)
121
+
122
+ 切换配置时,工具会自动更新 `settings.json`,同时保留你的 hooks 等其他设置。
123
+
124
+ ## 🔄 工作原理
125
+
126
+ 1. **交互式切换** (`ccs ls`):显示配置列表,让你选择要切换的配置
127
+ 2. **快速切换** (`ccs next`):自动切换到下一个配置,循环往复
128
+
129
+ ## 🤝 贡献
130
+
131
+ 欢迎提交 Issue 和 Pull Request!
132
+
133
+ ## 📄 许可证
134
+
135
+ MIT License
136
+
137
+ ## 🙏 致谢
138
+
139
+ 本项目基于 [claude-config-switch](https://github.com/canglong/claude-code-switch) 开发,感谢原作者的贡献。
140
+
141
+ ## 📝 更新日志
142
+
143
+ ### v1.0.0
144
+ - ✨ 新增 `ccs next` / `ccs n` 命令,支持快速循环切换配置
145
+ - 🔧 保持原有交互式切换功能
146
+ - 📦 重新打包发布
package/health.js ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ * 健康检查模块
3
+ * 提供 ccs health 命令:读取 ~/.claude/apiConfigs.json,依次检测各端点健康与网络延迟
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const https = require('https');
10
+ const chalk = require('chalk');
11
+
12
+ // 配置路径
13
+ const CONFIG_DIR = path.join(os.homedir(), '.claude');
14
+ const API_CONFIGS_FILE = path.join(CONFIG_DIR, 'apiConfigs.json');
15
+
16
+ /**
17
+ * 读取API配置文件
18
+ * @returns {Array} API配置数组(可能为空)
19
+ */
20
+ function readApiConfigs() {
21
+ try {
22
+ if (!fs.existsSync(API_CONFIGS_FILE)) {
23
+ console.log(chalk.yellow(`警告: API配置文件不存在 (${API_CONFIGS_FILE})`));
24
+ return [];
25
+ }
26
+ const data = fs.readFileSync(API_CONFIGS_FILE, 'utf8');
27
+ return JSON.parse(data);
28
+ } catch (error) {
29
+ console.error(chalk.red(`读取API配置文件失败: ${error.message}`));
30
+ return [];
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 探测指定端点
36
+ * - 将 2xx 与 4xx 视为可达(成功)
37
+ * - 将 5xx 或网络错误视为失败
38
+ * - 返回 { ok, statusCode, latencyMs, error, endpoint }
39
+ * @param {string} baseUrl 基础URL,如 https://api.anthropic.com
40
+ * @param {string} authToken API密钥用于鉴权
41
+ * @param {string} endpoint 端点路径,如 '/v1/models'
42
+ * @param {object} options 额外选项
43
+ * @returns {Promise<{ok:boolean,statusCode:number|undefined,latencyMs:number|undefined,error:Error|undefined,endpoint:string}>}
44
+ */
45
+ function probeEndpoint(baseUrl, authToken, endpoint = '/v1/models', options = {}) {
46
+ return new Promise((resolve) => {
47
+ let urlString = baseUrl.endsWith('/') ? `${baseUrl}${endpoint.substring(1)}` : `${baseUrl}${endpoint}`;
48
+ let timedOut = false;
49
+ const start = Date.now();
50
+
51
+ try {
52
+ const urlObj = new URL(urlString);
53
+ const requestOptions = {
54
+ hostname: urlObj.hostname,
55
+ port: urlObj.port || 443,
56
+ path: urlObj.pathname + urlObj.search,
57
+ method: options.method || 'GET',
58
+ headers: {
59
+ 'Accept': 'application/json',
60
+ 'Authorization': `Bearer ${authToken}`,
61
+ // 'x-api-key': authToken,
62
+ ...(options.includeAnthropicVersion !== false && { 'anthropic-version': '2023-06-01' }),
63
+ ...(options.contentType && { 'Content-Type': options.contentType }),
64
+ ...options.extraHeaders
65
+ },
66
+ timeout: 30000
67
+ };
68
+
69
+ const req = https.request(requestOptions, (res) => {
70
+ // 消耗响应体以完成请求
71
+ res.on('data', () => {});
72
+ res.on('end', () => {
73
+ const latencyMs = Date.now() - start;
74
+ const status = res.statusCode || 0;
75
+ const ok = status < 500; // 2xx/4xx 视为可达
76
+ resolve({ ok, statusCode: status, latencyMs, error: undefined, endpoint });
77
+ });
78
+ });
79
+
80
+ req.on('timeout', () => {
81
+ timedOut = true;
82
+ req.destroy(new Error('Request timeout'));
83
+ });
84
+
85
+ req.on('error', (err) => {
86
+ const latencyMs = Date.now() - start;
87
+ resolve({ ok: false, statusCode: undefined, latencyMs, error: timedOut ? new Error('timeout') : err, endpoint });
88
+ });
89
+
90
+ if (options.body) {
91
+ req.write(options.body);
92
+ }
93
+ req.end();
94
+ } catch (e) {
95
+ resolve({ ok: false, statusCode: undefined, latencyMs: undefined, error: e, endpoint });
96
+ }
97
+ });
98
+ }
99
+
100
+ /**
101
+ * 对单个配置执行智能健康检查
102
+ * 如果 /v1/models 返回 404,会尝试其他常见端点
103
+ * @param {object} configItem apiConfigs.json中单项
104
+ * @returns {Promise<{name:string, baseUrl:string, tokenPresent:boolean, healthy:boolean, statusCodes:number[], latencies:number[], error:Error|undefined, endpoint:string}>}
105
+ */
106
+ async function checkConfigHealth(configItem) {
107
+ const name = configItem?.name || 'unknown';
108
+ const env = configItem?.config?.env || {};
109
+ const baseUrl = env.ANTHROPIC_BASE_URL || '';
110
+ const tokenPresent = Boolean(env.ANTHROPIC_AUTH_TOKEN);
111
+ const authToken = env.ANTHROPIC_AUTH_TOKEN || '';
112
+
113
+ // 定义要尝试的端点
114
+ const endpointsToTry = [
115
+ { path: '/v1/models', description: 'Claude Models API' },
116
+ { path: '/v1/chat/completions', description: 'OpenAI Compatible API',
117
+ options: {
118
+ method: 'POST',
119
+ contentType: 'application/json',
120
+ body: JSON.stringify({
121
+ model: 'claude-3-sonnet-20240229',
122
+ messages: [{ role: 'user', content: 'test' }],
123
+ max_tokens: 1
124
+ })
125
+ }
126
+ },
127
+ { path: '/v1/models', description: 'No Anthropic Version', options: { includeAnthropicVersion: false } },
128
+ { path: '/', description: 'Root Path' },
129
+ { path: '/health', description: 'Health Check' },
130
+ { path: '/api/v1/models', description: 'Alternative API Path' }
131
+ ];
132
+
133
+ let bestResult = null;
134
+ let testedEndpoints = [];
135
+
136
+ for (const endpointConfig of endpointsToTry) {
137
+ const result = await probeEndpoint(baseUrl, authToken, endpointConfig.path, endpointConfig.options || {});
138
+ testedEndpoints.push(`${endpointConfig.path}:${result.statusCode}`);
139
+
140
+ // 如果是 2xx 状态码,直接返回成功结果
141
+ if (result.statusCode >= 200 && result.statusCode < 300) {
142
+ return {
143
+ name,
144
+ baseUrl,
145
+ tokenPresent,
146
+ healthy: true,
147
+ statusCodes: [result.statusCode],
148
+ latencies: [result.latencyMs],
149
+ error: undefined,
150
+ endpoint: `${endpointConfig.path} (${endpointConfig.description})`
151
+ };
152
+ }
153
+
154
+ // 记录最好的结果(最低的错误代码或最早的可达结果)
155
+ if (!bestResult || (result.ok && !bestResult.ok) ||
156
+ (result.statusCode && (!bestResult.statusCode || result.statusCode < bestResult.statusCode))) {
157
+ bestResult = { ...result, description: endpointConfig.description };
158
+ }
159
+ }
160
+
161
+ // 如果没有找到 2xx 响应,返回最好的结果
162
+ const healthy = bestResult ? bestResult.ok : false;
163
+ return {
164
+ name,
165
+ baseUrl,
166
+ tokenPresent,
167
+ healthy,
168
+ statusCodes: bestResult ? [bestResult.statusCode] : [],
169
+ latencies: bestResult ? [bestResult.latencyMs] : [],
170
+ error: bestResult?.error,
171
+ endpoint: bestResult ? `${bestResult.endpoint} (${bestResult.description})` : 'All endpoints failed'
172
+ };
173
+ }
174
+
175
+ /**
176
+ * 掩码显示API Key,显示前7位+****
177
+ * @param {boolean} present 是否存在
178
+ * @param {string} token 原始token
179
+ * @returns {string} 掩码显示
180
+ */
181
+ function maskToken(present, token) {
182
+ if (!present) return 'N/A';
183
+ if (!token || token.length < 7) return '****';
184
+ return `${token.slice(0, 7)}****`;
185
+ }
186
+
187
+ /**
188
+ * 注册 health 命令
189
+ * @param {import('commander').Command} program Commander实例
190
+ */
191
+ function registerHealthCommands(program) {
192
+ program
193
+ .command('health')
194
+ .description('检查各API端点的可用性与网络延迟')
195
+ .action(async () => {
196
+ const apiConfigs = readApiConfigs();
197
+ if (apiConfigs.length === 0) {
198
+ console.log(chalk.yellow('没有找到可用的API配置'));
199
+ return;
200
+ }
201
+
202
+ console.log(chalk.cyan('开始健康检查 (/v1/models)...\n'));
203
+
204
+ // 去重URL,只检测每个URL的第一个配置
205
+ const uniqueUrls = new Set();
206
+ const configsToCheck = [];
207
+
208
+ apiConfigs.forEach(item => {
209
+ const env = item?.config?.env || {};
210
+ const baseUrl = env.ANTHROPIC_BASE_URL || '';
211
+ const token = env.ANTHROPIC_AUTH_TOKEN || '';
212
+
213
+ // 只处理首次出现的URL
214
+ if (!uniqueUrls.has(baseUrl) && baseUrl) {
215
+ uniqueUrls.add(baseUrl);
216
+ configsToCheck.push({
217
+ item,
218
+ token,
219
+ baseUrl
220
+ });
221
+ }
222
+ });
223
+
224
+ // 固定宽度设置
225
+ const nameWidth = 18;
226
+ const fixedUrlWidth = 30;
227
+ const tokenWidth = 12;
228
+ const statusWidth = 22;
229
+
230
+ // 表头
231
+ const nameHeader = 'Name'.padEnd(nameWidth);
232
+ const urlHeader = 'Base URL'.padEnd(fixedUrlWidth);
233
+ const tokenHeader = 'Token'.padEnd(tokenWidth);
234
+ const statusHeader = 'Status'.padEnd(statusWidth);
235
+ const latencyHeader = 'Latency';
236
+
237
+ console.log(chalk.bold(`| ${nameHeader} | ${urlHeader} | ${tokenHeader} | ${statusHeader} | ${latencyHeader} |`));
238
+ console.log(`|${'-'.repeat(nameWidth + 2)}|${'-'.repeat(fixedUrlWidth + 2)}|${'-'.repeat(tokenWidth + 2)}|${'-'.repeat(statusWidth + 2)}|${'-'.repeat(10)}|`);
239
+
240
+ // 逐个检测并输出
241
+ for (const config of configsToCheck) {
242
+ const name = (config.item?.name || 'unknown').length > nameWidth
243
+ ? (config.item?.name || 'unknown').substring(0, nameWidth - 3) + '...'
244
+ : (config.item?.name || 'unknown').padEnd(nameWidth);
245
+ const url = config.baseUrl.length > fixedUrlWidth
246
+ ? config.baseUrl.substring(0, fixedUrlWidth - 3) + '...'
247
+ : config.baseUrl.padEnd(fixedUrlWidth);
248
+ const tokenMasked = maskToken(Boolean(config.token), config.token).padEnd(tokenWidth);
249
+
250
+ // 显示检测中状态
251
+ const checkingStatus = chalk.yellow('Checking...').padEnd(statusWidth + (chalk.yellow('Checking...').length - 'Checking...'.length));
252
+ process.stdout.write(`| ${name} | ${url} | ${tokenMasked} | ${checkingStatus} | ... |\r`);
253
+
254
+ // 执行检测
255
+ const endpointResult = await checkConfigHealth(config.item);
256
+
257
+ const latencyText = endpointResult.latencies.length
258
+ ? `${Math.round(endpointResult.latencies[0])}ms`
259
+ : 'N/A';
260
+
261
+ const statusCode = endpointResult.statusCodes.length ? endpointResult.statusCodes[0] : 'N/A';
262
+ const healthStatus = endpointResult.healthy ? 'Healthy' : 'Unhealthy';
263
+ const statusText = `${healthStatus} (status: ${statusCode})`;
264
+
265
+ // 根据状态码着色
266
+ let coloredStatus;
267
+ if (statusCode >= 200 && statusCode < 300) {
268
+ coloredStatus = chalk.green(statusText);
269
+ } else if (statusCode >= 400 || (typeof statusCode === 'string' && statusCode !== 'N/A')) {
270
+ coloredStatus = chalk.red(statusText);
271
+ } else {
272
+ coloredStatus = chalk.yellow(statusText);
273
+ }
274
+
275
+ const statusFormatted = coloredStatus.padEnd(statusWidth + (coloredStatus.length - statusText.length));
276
+
277
+ // 清除当前行并输出最终结果
278
+ process.stdout.write('\r\x1b[K');
279
+ console.log(`| ${name} | ${url} | ${tokenMasked} | ${statusFormatted} | ${latencyText} |`);
280
+
281
+ if (!endpointResult.healthy && endpointResult.error) {
282
+ console.log(chalk.gray(` Error: ${endpointResult.error.message}`));
283
+ }
284
+
285
+ // // 显示找到的可用端点信息
286
+ // if (endpointResult.endpoint && endpointResult.endpoint !== '/v1/models (Claude Models API)') {
287
+ // console.log(chalk.cyan(` → Found working endpoint: ${endpointResult.endpoint}`));
288
+ // }
289
+ }
290
+ });
291
+ }
292
+
293
+ module.exports = { registerHealthCommands };
294
+