@tkpdx01/ccc 1.2.7 → 1.3.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/Claude Code settings.txt +776 -776
- package/README.md +77 -0
- package/index.js +4 -2
- package/package.json +40 -39
- package/src/commands/delete.js +66 -66
- package/src/commands/edit.js +120 -120
- package/src/commands/help.js +50 -50
- package/src/commands/import.js +356 -356
- package/src/commands/index.js +11 -10
- package/src/commands/list.js +46 -46
- package/src/commands/new.js +109 -109
- package/src/commands/show.js +68 -68
- package/src/commands/sync.js +93 -93
- package/src/commands/use.js +19 -19
- package/src/commands/webdav.js +477 -0
- package/src/config.js +9 -9
- package/src/crypto.js +147 -0
- package/src/launch.js +69 -69
- package/src/parsers.js +154 -154
- package/src/profiles.js +182 -182
- package/src/utils.js +67 -67
- package/src/webdav.js +268 -0
- package/.claude/settings.local.json +0 -34
- package/nul +0 -1
package/src/commands/import.js
CHANGED
|
@@ -1,356 +1,356 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import readline from 'readline';
|
|
6
|
-
import Table from 'cli-table3';
|
|
7
|
-
import {
|
|
8
|
-
ensureDirs,
|
|
9
|
-
getProfiles,
|
|
10
|
-
getProfilePath,
|
|
11
|
-
setDefaultProfile,
|
|
12
|
-
profileExists
|
|
13
|
-
} from '../profiles.js';
|
|
14
|
-
import { parseCCSwitchSQL, parseAllApiHubJSON, detectFileFormat } from '../parsers.js';
|
|
15
|
-
import { extractFromText, getDomainName, sanitizeProfileName, convertToClaudeSettings } from '../utils.js';
|
|
16
|
-
|
|
17
|
-
// 生成唯一的 profile 名称(如果重复则加后缀 token2, token3...)
|
|
18
|
-
function getUniqueProfileName(baseName, usedNames) {
|
|
19
|
-
if (!usedNames.has(baseName)) {
|
|
20
|
-
usedNames.add(baseName);
|
|
21
|
-
return baseName;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let suffix = 2;
|
|
25
|
-
let newName = `${baseName}-token${suffix}`;
|
|
26
|
-
while (usedNames.has(newName)) {
|
|
27
|
-
suffix++;
|
|
28
|
-
newName = `${baseName}-token${suffix}`;
|
|
29
|
-
}
|
|
30
|
-
usedNames.add(newName);
|
|
31
|
-
return newName;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 交互式导入命令
|
|
35
|
-
export function importCommand(program) {
|
|
36
|
-
// ccc import <file> - 从文件导入(自动识别格式)
|
|
37
|
-
program
|
|
38
|
-
.command('import <file>')
|
|
39
|
-
.aliases(['if'])
|
|
40
|
-
.description('从文件导入配置(自动识别 CC-Switch SQL 或 All API Hub JSON)')
|
|
41
|
-
.action(async (file) => {
|
|
42
|
-
// 检查文件是否存在
|
|
43
|
-
const filePath = path.resolve(file);
|
|
44
|
-
if (!fs.existsSync(filePath)) {
|
|
45
|
-
console.log(chalk.red(`文件不存在: ${filePath}`));
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.log(chalk.cyan(`读取文件: ${filePath}\n`));
|
|
50
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
51
|
-
|
|
52
|
-
// 自动检测格式
|
|
53
|
-
const format = detectFileFormat(content);
|
|
54
|
-
|
|
55
|
-
if (!format) {
|
|
56
|
-
console.log(chalk.red('无法识别文件格式'));
|
|
57
|
-
console.log(chalk.gray('支持的格式:'));
|
|
58
|
-
console.log(chalk.gray(' - CC-Switch SQL 导出文件 (.sql)'));
|
|
59
|
-
console.log(chalk.gray(' - All API Hub JSON 导出文件 (.json)'));
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let providers = [];
|
|
64
|
-
let formatName = '';
|
|
65
|
-
|
|
66
|
-
if (format === 'ccswitch') {
|
|
67
|
-
formatName = 'CC-Switch SQL';
|
|
68
|
-
providers = parseCCSwitchSQL(content);
|
|
69
|
-
} else if (format === 'allapihub') {
|
|
70
|
-
formatName = 'All API Hub JSON';
|
|
71
|
-
providers = parseAllApiHubJSON(content);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (providers.length === 0) {
|
|
75
|
-
console.log(chalk.yellow('未找到有效的配置'));
|
|
76
|
-
process.exit(0);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
console.log(chalk.green(`✓ 识别到 ${formatName} 格式`));
|
|
80
|
-
console.log(chalk.green(`✓ 找到 ${providers.length} 个配置\n`));
|
|
81
|
-
|
|
82
|
-
// 显示找到的配置
|
|
83
|
-
const table = new Table({
|
|
84
|
-
head: [chalk.cyan('#'), chalk.cyan('Profile 名称'), chalk.cyan('API URL'), chalk.cyan('备注')],
|
|
85
|
-
style: { head: [], border: [] }
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// 用于跟踪已使用的名称(预览阶段)
|
|
89
|
-
const previewUsedNames = new Set();
|
|
90
|
-
|
|
91
|
-
providers.forEach((p, i) => {
|
|
92
|
-
const url = p.settingsConfig?.env?.ANTHROPIC_BASE_URL || p.websiteUrl || '(未设置)';
|
|
93
|
-
// 使用 API URL 生成 profile 名称,重复时加后缀
|
|
94
|
-
const baseName = sanitizeProfileName(getDomainName(url) || p.name);
|
|
95
|
-
const profileName = getUniqueProfileName(baseName, previewUsedNames);
|
|
96
|
-
let note = '';
|
|
97
|
-
|
|
98
|
-
if (format === 'ccswitch') {
|
|
99
|
-
note = p.settingsConfig?.model || '(默认模型)';
|
|
100
|
-
} else if (format === 'allapihub') {
|
|
101
|
-
note = p.meta?.health === 'healthy' ? chalk.green('健康') :
|
|
102
|
-
p.meta?.health === 'warning' ? chalk.yellow('警告') :
|
|
103
|
-
p.meta?.health === 'error' ? chalk.red('错误') : chalk.gray('未知');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
table.push([i + 1, profileName, url, note]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
console.log(table.toString());
|
|
110
|
-
console.log();
|
|
111
|
-
|
|
112
|
-
// All API Hub 特殊警告
|
|
113
|
-
if (format === 'allapihub') {
|
|
114
|
-
console.log(chalk.yellow('⚠ 注意: All API Hub 的 access_token 格式可能需要手动调整'));
|
|
115
|
-
console.log(chalk.gray(' 导入后可使用 "ccc edit <profile>" 修改 API Key\n'));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 确认导入
|
|
119
|
-
const { confirmImport } = await inquirer.prompt([
|
|
120
|
-
{
|
|
121
|
-
type: 'confirm',
|
|
122
|
-
name: 'confirmImport',
|
|
123
|
-
message: `确定要导入这 ${providers.length} 个配置吗?`,
|
|
124
|
-
default: true
|
|
125
|
-
}
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
if (!confirmImport) {
|
|
129
|
-
console.log(chalk.yellow('已取消'));
|
|
130
|
-
process.exit(0);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 选择要导入的配置
|
|
134
|
-
// 重新计算名称用于选择列表
|
|
135
|
-
const selectionUsedNames = new Set();
|
|
136
|
-
const { selection } = await inquirer.prompt([
|
|
137
|
-
{
|
|
138
|
-
type: 'checkbox',
|
|
139
|
-
name: 'selection',
|
|
140
|
-
message: '选择要导入的配置 (空格选择,回车确认):',
|
|
141
|
-
choices: providers.map((p, i) => {
|
|
142
|
-
const url = p.settingsConfig?.env?.ANTHROPIC_BASE_URL || p.websiteUrl || '';
|
|
143
|
-
// 使用 API URL 生成 profile 名称,重复时加后缀
|
|
144
|
-
const baseName = sanitizeProfileName(getDomainName(url) || p.name);
|
|
145
|
-
const profileName = getUniqueProfileName(baseName, selectionUsedNames);
|
|
146
|
-
return {
|
|
147
|
-
name: `${profileName} (${url})`,
|
|
148
|
-
value: i,
|
|
149
|
-
checked: format === 'allapihub' ? p.meta?.health === 'healthy' : true
|
|
150
|
-
};
|
|
151
|
-
})
|
|
152
|
-
}
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
const selectedProviders = selection.map(i => providers[i]);
|
|
156
|
-
|
|
157
|
-
if (selectedProviders.length === 0) {
|
|
158
|
-
console.log(chalk.yellow('未选择任何配置'));
|
|
159
|
-
process.exit(0);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 导入选中的配置
|
|
163
|
-
ensureDirs();
|
|
164
|
-
let imported = 0;
|
|
165
|
-
let skipped = 0;
|
|
166
|
-
|
|
167
|
-
// 用于跟踪导入时已使用的名称(包括已存在的 profiles)
|
|
168
|
-
const importUsedNames = new Set(getProfiles());
|
|
169
|
-
|
|
170
|
-
for (const provider of selectedProviders) {
|
|
171
|
-
const url = provider.settingsConfig?.env?.ANTHROPIC_BASE_URL || provider.websiteUrl || '';
|
|
172
|
-
// 使用 API URL 生成 profile 名称,重复时加后缀
|
|
173
|
-
const baseName = sanitizeProfileName(getDomainName(url) || provider.name);
|
|
174
|
-
const profileName = getUniqueProfileName(baseName, importUsedNames);
|
|
175
|
-
const profilePath = getProfilePath(profileName);
|
|
176
|
-
|
|
177
|
-
// 检查是否已存在
|
|
178
|
-
if (fs.existsSync(profilePath)) {
|
|
179
|
-
const { overwrite } = await inquirer.prompt([
|
|
180
|
-
{
|
|
181
|
-
type: 'confirm',
|
|
182
|
-
name: 'overwrite',
|
|
183
|
-
message: `配置 "${profileName}" 已存在,是否覆盖?`,
|
|
184
|
-
default: false
|
|
185
|
-
}
|
|
186
|
-
]);
|
|
187
|
-
|
|
188
|
-
if (!overwrite) {
|
|
189
|
-
skipped++;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 转换并保存配置(影子配置只包含 API 凭证)
|
|
195
|
-
const settings = convertToClaudeSettings(provider);
|
|
196
|
-
fs.writeFileSync(profilePath, JSON.stringify(settings, null, 2));
|
|
197
|
-
console.log(chalk.green(`✓ ${profileName}`));
|
|
198
|
-
imported++;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
console.log(chalk.green(`\n✓ 导入完成: ${imported} 个成功` + (skipped > 0 ? `, ${skipped} 个跳过` : '')));
|
|
202
|
-
|
|
203
|
-
// 如果是第一次导入,设置默认
|
|
204
|
-
const profiles = getProfiles();
|
|
205
|
-
if (profiles.length === imported && imported > 0) {
|
|
206
|
-
const { setDefault } = await inquirer.prompt([
|
|
207
|
-
{
|
|
208
|
-
type: 'confirm',
|
|
209
|
-
name: 'setDefault',
|
|
210
|
-
message: '是否设置第一个配置为默认?',
|
|
211
|
-
default: true
|
|
212
|
-
}
|
|
213
|
-
]);
|
|
214
|
-
|
|
215
|
-
if (setDefault) {
|
|
216
|
-
setDefaultProfile(profiles[0]);
|
|
217
|
-
console.log(chalk.green(`✓ 已设置 "${profiles[0]}" 为默认配置`));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// 交互式粘贴导入(原 importProfile 功能,可选添加)
|
|
224
|
-
export async function interactiveImport() {
|
|
225
|
-
console.log(chalk.cyan('请粘贴包含 API URL 和 SK Token 的文本,然后按两次回车确认:'));
|
|
226
|
-
console.log(chalk.gray('(支持自动识别 URL 和 sk-xxx 格式的 token)'));
|
|
227
|
-
console.log();
|
|
228
|
-
|
|
229
|
-
const rl = readline.createInterface({
|
|
230
|
-
input: process.stdin,
|
|
231
|
-
output: process.stdout
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
let inputText = '';
|
|
235
|
-
let emptyLineCount = 0;
|
|
236
|
-
|
|
237
|
-
const text = await new Promise((resolve) => {
|
|
238
|
-
rl.on('line', (line) => {
|
|
239
|
-
if (line === '') {
|
|
240
|
-
emptyLineCount++;
|
|
241
|
-
if (emptyLineCount >= 2) {
|
|
242
|
-
rl.close();
|
|
243
|
-
resolve(inputText);
|
|
244
|
-
}
|
|
245
|
-
} else {
|
|
246
|
-
emptyLineCount = 0;
|
|
247
|
-
inputText += line + '\n';
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const { urls, tokens } = extractFromText(text);
|
|
253
|
-
|
|
254
|
-
if (urls.length === 0 && tokens.length === 0) {
|
|
255
|
-
console.log(chalk.red('未找到有效的 URL 或 Token'));
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
console.log();
|
|
260
|
-
console.log(chalk.green('识别到的内容:'));
|
|
261
|
-
|
|
262
|
-
if (urls.length > 0) {
|
|
263
|
-
console.log(chalk.cyan('URLs:'));
|
|
264
|
-
urls.forEach(u => console.log(` - ${u}`));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (tokens.length > 0) {
|
|
268
|
-
console.log(chalk.cyan('Tokens:'));
|
|
269
|
-
tokens.forEach(t => console.log(` - ${t.substring(0, 10)}...`));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// 使用第一个 URL 的域名作为默认名称
|
|
273
|
-
let defaultName = 'custom';
|
|
274
|
-
if (urls.length > 0) {
|
|
275
|
-
const domainName = getDomainName(urls[0]);
|
|
276
|
-
if (domainName) {
|
|
277
|
-
defaultName = domainName;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const { profileName, apiUrl, apiKey } = await inquirer.prompt([
|
|
282
|
-
{
|
|
283
|
-
type: 'input',
|
|
284
|
-
name: 'profileName',
|
|
285
|
-
message: 'Profile 名称:',
|
|
286
|
-
default: defaultName
|
|
287
|
-
},
|
|
288
|
-
{
|
|
289
|
-
type: 'list',
|
|
290
|
-
name: 'apiUrl',
|
|
291
|
-
message: '选择 API URL:',
|
|
292
|
-
choices: urls.length > 0 ? urls : ['https://api.anthropic.com'],
|
|
293
|
-
when: urls.length > 0
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
type: 'input',
|
|
297
|
-
name: 'apiUrl',
|
|
298
|
-
message: 'API URL:',
|
|
299
|
-
default: 'https://api.anthropic.com',
|
|
300
|
-
when: urls.length === 0
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
type: 'list',
|
|
304
|
-
name: 'apiKey',
|
|
305
|
-
message: '选择 API Key:',
|
|
306
|
-
choices: tokens.map(t => ({ name: `${t.substring(0, 15)}...`, value: t })),
|
|
307
|
-
when: tokens.length > 1
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
type: 'input',
|
|
311
|
-
name: 'apiKey',
|
|
312
|
-
message: 'API Key:',
|
|
313
|
-
default: tokens[0] || '',
|
|
314
|
-
when: tokens.length <= 1
|
|
315
|
-
}
|
|
316
|
-
]);
|
|
317
|
-
|
|
318
|
-
const finalApiUrl = apiUrl || 'https://api.anthropic.com';
|
|
319
|
-
const finalApiKey = apiKey || tokens[0] || '';
|
|
320
|
-
|
|
321
|
-
// 影子配置只存储 API 凭证
|
|
322
|
-
const settings = {
|
|
323
|
-
ANTHROPIC_AUTH_TOKEN: finalApiKey,
|
|
324
|
-
ANTHROPIC_BASE_URL: finalApiUrl
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
ensureDirs();
|
|
328
|
-
const profilePath = getProfilePath(profileName);
|
|
329
|
-
|
|
330
|
-
if (fs.existsSync(profilePath)) {
|
|
331
|
-
const { overwrite } = await inquirer.prompt([
|
|
332
|
-
{
|
|
333
|
-
type: 'confirm',
|
|
334
|
-
name: 'overwrite',
|
|
335
|
-
message: `Profile "${profileName}" 已存在,是否覆盖?`,
|
|
336
|
-
default: false
|
|
337
|
-
}
|
|
338
|
-
]);
|
|
339
|
-
|
|
340
|
-
if (!overwrite) {
|
|
341
|
-
console.log(chalk.yellow('已取消'));
|
|
342
|
-
process.exit(0);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
fs.writeFileSync(profilePath, JSON.stringify(settings, null, 2));
|
|
347
|
-
console.log(chalk.green(`\n✓ Profile "${profileName}" 已保存到 ${profilePath}`));
|
|
348
|
-
|
|
349
|
-
// 如果是第一个 profile,设为默认
|
|
350
|
-
const profiles = getProfiles();
|
|
351
|
-
if (profiles.length === 1) {
|
|
352
|
-
setDefaultProfile(profileName);
|
|
353
|
-
console.log(chalk.green(`✓ 已设为默认 profile`));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
import {
|
|
8
|
+
ensureDirs,
|
|
9
|
+
getProfiles,
|
|
10
|
+
getProfilePath,
|
|
11
|
+
setDefaultProfile,
|
|
12
|
+
profileExists
|
|
13
|
+
} from '../profiles.js';
|
|
14
|
+
import { parseCCSwitchSQL, parseAllApiHubJSON, detectFileFormat } from '../parsers.js';
|
|
15
|
+
import { extractFromText, getDomainName, sanitizeProfileName, convertToClaudeSettings } from '../utils.js';
|
|
16
|
+
|
|
17
|
+
// 生成唯一的 profile 名称(如果重复则加后缀 token2, token3...)
|
|
18
|
+
function getUniqueProfileName(baseName, usedNames) {
|
|
19
|
+
if (!usedNames.has(baseName)) {
|
|
20
|
+
usedNames.add(baseName);
|
|
21
|
+
return baseName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let suffix = 2;
|
|
25
|
+
let newName = `${baseName}-token${suffix}`;
|
|
26
|
+
while (usedNames.has(newName)) {
|
|
27
|
+
suffix++;
|
|
28
|
+
newName = `${baseName}-token${suffix}`;
|
|
29
|
+
}
|
|
30
|
+
usedNames.add(newName);
|
|
31
|
+
return newName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 交互式导入命令
|
|
35
|
+
export function importCommand(program) {
|
|
36
|
+
// ccc import <file> - 从文件导入(自动识别格式)
|
|
37
|
+
program
|
|
38
|
+
.command('import <file>')
|
|
39
|
+
.aliases(['if'])
|
|
40
|
+
.description('从文件导入配置(自动识别 CC-Switch SQL 或 All API Hub JSON)')
|
|
41
|
+
.action(async (file) => {
|
|
42
|
+
// 检查文件是否存在
|
|
43
|
+
const filePath = path.resolve(file);
|
|
44
|
+
if (!fs.existsSync(filePath)) {
|
|
45
|
+
console.log(chalk.red(`文件不存在: ${filePath}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(chalk.cyan(`读取文件: ${filePath}\n`));
|
|
50
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
51
|
+
|
|
52
|
+
// 自动检测格式
|
|
53
|
+
const format = detectFileFormat(content);
|
|
54
|
+
|
|
55
|
+
if (!format) {
|
|
56
|
+
console.log(chalk.red('无法识别文件格式'));
|
|
57
|
+
console.log(chalk.gray('支持的格式:'));
|
|
58
|
+
console.log(chalk.gray(' - CC-Switch SQL 导出文件 (.sql)'));
|
|
59
|
+
console.log(chalk.gray(' - All API Hub JSON 导出文件 (.json)'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let providers = [];
|
|
64
|
+
let formatName = '';
|
|
65
|
+
|
|
66
|
+
if (format === 'ccswitch') {
|
|
67
|
+
formatName = 'CC-Switch SQL';
|
|
68
|
+
providers = parseCCSwitchSQL(content);
|
|
69
|
+
} else if (format === 'allapihub') {
|
|
70
|
+
formatName = 'All API Hub JSON';
|
|
71
|
+
providers = parseAllApiHubJSON(content);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (providers.length === 0) {
|
|
75
|
+
console.log(chalk.yellow('未找到有效的配置'));
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(chalk.green(`✓ 识别到 ${formatName} 格式`));
|
|
80
|
+
console.log(chalk.green(`✓ 找到 ${providers.length} 个配置\n`));
|
|
81
|
+
|
|
82
|
+
// 显示找到的配置
|
|
83
|
+
const table = new Table({
|
|
84
|
+
head: [chalk.cyan('#'), chalk.cyan('Profile 名称'), chalk.cyan('API URL'), chalk.cyan('备注')],
|
|
85
|
+
style: { head: [], border: [] }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 用于跟踪已使用的名称(预览阶段)
|
|
89
|
+
const previewUsedNames = new Set();
|
|
90
|
+
|
|
91
|
+
providers.forEach((p, i) => {
|
|
92
|
+
const url = p.settingsConfig?.env?.ANTHROPIC_BASE_URL || p.websiteUrl || '(未设置)';
|
|
93
|
+
// 使用 API URL 生成 profile 名称,重复时加后缀
|
|
94
|
+
const baseName = sanitizeProfileName(getDomainName(url) || p.name);
|
|
95
|
+
const profileName = getUniqueProfileName(baseName, previewUsedNames);
|
|
96
|
+
let note = '';
|
|
97
|
+
|
|
98
|
+
if (format === 'ccswitch') {
|
|
99
|
+
note = p.settingsConfig?.model || '(默认模型)';
|
|
100
|
+
} else if (format === 'allapihub') {
|
|
101
|
+
note = p.meta?.health === 'healthy' ? chalk.green('健康') :
|
|
102
|
+
p.meta?.health === 'warning' ? chalk.yellow('警告') :
|
|
103
|
+
p.meta?.health === 'error' ? chalk.red('错误') : chalk.gray('未知');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
table.push([i + 1, profileName, url, note]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log(table.toString());
|
|
110
|
+
console.log();
|
|
111
|
+
|
|
112
|
+
// All API Hub 特殊警告
|
|
113
|
+
if (format === 'allapihub') {
|
|
114
|
+
console.log(chalk.yellow('⚠ 注意: All API Hub 的 access_token 格式可能需要手动调整'));
|
|
115
|
+
console.log(chalk.gray(' 导入后可使用 "ccc edit <profile>" 修改 API Key\n'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 确认导入
|
|
119
|
+
const { confirmImport } = await inquirer.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: 'confirm',
|
|
122
|
+
name: 'confirmImport',
|
|
123
|
+
message: `确定要导入这 ${providers.length} 个配置吗?`,
|
|
124
|
+
default: true
|
|
125
|
+
}
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
if (!confirmImport) {
|
|
129
|
+
console.log(chalk.yellow('已取消'));
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 选择要导入的配置
|
|
134
|
+
// 重新计算名称用于选择列表
|
|
135
|
+
const selectionUsedNames = new Set();
|
|
136
|
+
const { selection } = await inquirer.prompt([
|
|
137
|
+
{
|
|
138
|
+
type: 'checkbox',
|
|
139
|
+
name: 'selection',
|
|
140
|
+
message: '选择要导入的配置 (空格选择,回车确认):',
|
|
141
|
+
choices: providers.map((p, i) => {
|
|
142
|
+
const url = p.settingsConfig?.env?.ANTHROPIC_BASE_URL || p.websiteUrl || '';
|
|
143
|
+
// 使用 API URL 生成 profile 名称,重复时加后缀
|
|
144
|
+
const baseName = sanitizeProfileName(getDomainName(url) || p.name);
|
|
145
|
+
const profileName = getUniqueProfileName(baseName, selectionUsedNames);
|
|
146
|
+
return {
|
|
147
|
+
name: `${profileName} (${url})`,
|
|
148
|
+
value: i,
|
|
149
|
+
checked: format === 'allapihub' ? p.meta?.health === 'healthy' : true
|
|
150
|
+
};
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
const selectedProviders = selection.map(i => providers[i]);
|
|
156
|
+
|
|
157
|
+
if (selectedProviders.length === 0) {
|
|
158
|
+
console.log(chalk.yellow('未选择任何配置'));
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 导入选中的配置
|
|
163
|
+
ensureDirs();
|
|
164
|
+
let imported = 0;
|
|
165
|
+
let skipped = 0;
|
|
166
|
+
|
|
167
|
+
// 用于跟踪导入时已使用的名称(包括已存在的 profiles)
|
|
168
|
+
const importUsedNames = new Set(getProfiles());
|
|
169
|
+
|
|
170
|
+
for (const provider of selectedProviders) {
|
|
171
|
+
const url = provider.settingsConfig?.env?.ANTHROPIC_BASE_URL || provider.websiteUrl || '';
|
|
172
|
+
// 使用 API URL 生成 profile 名称,重复时加后缀
|
|
173
|
+
const baseName = sanitizeProfileName(getDomainName(url) || provider.name);
|
|
174
|
+
const profileName = getUniqueProfileName(baseName, importUsedNames);
|
|
175
|
+
const profilePath = getProfilePath(profileName);
|
|
176
|
+
|
|
177
|
+
// 检查是否已存在
|
|
178
|
+
if (fs.existsSync(profilePath)) {
|
|
179
|
+
const { overwrite } = await inquirer.prompt([
|
|
180
|
+
{
|
|
181
|
+
type: 'confirm',
|
|
182
|
+
name: 'overwrite',
|
|
183
|
+
message: `配置 "${profileName}" 已存在,是否覆盖?`,
|
|
184
|
+
default: false
|
|
185
|
+
}
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
if (!overwrite) {
|
|
189
|
+
skipped++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 转换并保存配置(影子配置只包含 API 凭证)
|
|
195
|
+
const settings = convertToClaudeSettings(provider);
|
|
196
|
+
fs.writeFileSync(profilePath, JSON.stringify(settings, null, 2));
|
|
197
|
+
console.log(chalk.green(`✓ ${profileName}`));
|
|
198
|
+
imported++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(chalk.green(`\n✓ 导入完成: ${imported} 个成功` + (skipped > 0 ? `, ${skipped} 个跳过` : '')));
|
|
202
|
+
|
|
203
|
+
// 如果是第一次导入,设置默认
|
|
204
|
+
const profiles = getProfiles();
|
|
205
|
+
if (profiles.length === imported && imported > 0) {
|
|
206
|
+
const { setDefault } = await inquirer.prompt([
|
|
207
|
+
{
|
|
208
|
+
type: 'confirm',
|
|
209
|
+
name: 'setDefault',
|
|
210
|
+
message: '是否设置第一个配置为默认?',
|
|
211
|
+
default: true
|
|
212
|
+
}
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
if (setDefault) {
|
|
216
|
+
setDefaultProfile(profiles[0]);
|
|
217
|
+
console.log(chalk.green(`✓ 已设置 "${profiles[0]}" 为默认配置`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 交互式粘贴导入(原 importProfile 功能,可选添加)
|
|
224
|
+
export async function interactiveImport() {
|
|
225
|
+
console.log(chalk.cyan('请粘贴包含 API URL 和 SK Token 的文本,然后按两次回车确认:'));
|
|
226
|
+
console.log(chalk.gray('(支持自动识别 URL 和 sk-xxx 格式的 token)'));
|
|
227
|
+
console.log();
|
|
228
|
+
|
|
229
|
+
const rl = readline.createInterface({
|
|
230
|
+
input: process.stdin,
|
|
231
|
+
output: process.stdout
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let inputText = '';
|
|
235
|
+
let emptyLineCount = 0;
|
|
236
|
+
|
|
237
|
+
const text = await new Promise((resolve) => {
|
|
238
|
+
rl.on('line', (line) => {
|
|
239
|
+
if (line === '') {
|
|
240
|
+
emptyLineCount++;
|
|
241
|
+
if (emptyLineCount >= 2) {
|
|
242
|
+
rl.close();
|
|
243
|
+
resolve(inputText);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
emptyLineCount = 0;
|
|
247
|
+
inputText += line + '\n';
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const { urls, tokens } = extractFromText(text);
|
|
253
|
+
|
|
254
|
+
if (urls.length === 0 && tokens.length === 0) {
|
|
255
|
+
console.log(chalk.red('未找到有效的 URL 或 Token'));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log();
|
|
260
|
+
console.log(chalk.green('识别到的内容:'));
|
|
261
|
+
|
|
262
|
+
if (urls.length > 0) {
|
|
263
|
+
console.log(chalk.cyan('URLs:'));
|
|
264
|
+
urls.forEach(u => console.log(` - ${u}`));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (tokens.length > 0) {
|
|
268
|
+
console.log(chalk.cyan('Tokens:'));
|
|
269
|
+
tokens.forEach(t => console.log(` - ${t.substring(0, 10)}...`));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 使用第一个 URL 的域名作为默认名称
|
|
273
|
+
let defaultName = 'custom';
|
|
274
|
+
if (urls.length > 0) {
|
|
275
|
+
const domainName = getDomainName(urls[0]);
|
|
276
|
+
if (domainName) {
|
|
277
|
+
defaultName = domainName;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { profileName, apiUrl, apiKey } = await inquirer.prompt([
|
|
282
|
+
{
|
|
283
|
+
type: 'input',
|
|
284
|
+
name: 'profileName',
|
|
285
|
+
message: 'Profile 名称:',
|
|
286
|
+
default: defaultName
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: 'list',
|
|
290
|
+
name: 'apiUrl',
|
|
291
|
+
message: '选择 API URL:',
|
|
292
|
+
choices: urls.length > 0 ? urls : ['https://api.anthropic.com'],
|
|
293
|
+
when: urls.length > 0
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
type: 'input',
|
|
297
|
+
name: 'apiUrl',
|
|
298
|
+
message: 'API URL:',
|
|
299
|
+
default: 'https://api.anthropic.com',
|
|
300
|
+
when: urls.length === 0
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: 'list',
|
|
304
|
+
name: 'apiKey',
|
|
305
|
+
message: '选择 API Key:',
|
|
306
|
+
choices: tokens.map(t => ({ name: `${t.substring(0, 15)}...`, value: t })),
|
|
307
|
+
when: tokens.length > 1
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: 'input',
|
|
311
|
+
name: 'apiKey',
|
|
312
|
+
message: 'API Key:',
|
|
313
|
+
default: tokens[0] || '',
|
|
314
|
+
when: tokens.length <= 1
|
|
315
|
+
}
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
const finalApiUrl = apiUrl || 'https://api.anthropic.com';
|
|
319
|
+
const finalApiKey = apiKey || tokens[0] || '';
|
|
320
|
+
|
|
321
|
+
// 影子配置只存储 API 凭证
|
|
322
|
+
const settings = {
|
|
323
|
+
ANTHROPIC_AUTH_TOKEN: finalApiKey,
|
|
324
|
+
ANTHROPIC_BASE_URL: finalApiUrl
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
ensureDirs();
|
|
328
|
+
const profilePath = getProfilePath(profileName);
|
|
329
|
+
|
|
330
|
+
if (fs.existsSync(profilePath)) {
|
|
331
|
+
const { overwrite } = await inquirer.prompt([
|
|
332
|
+
{
|
|
333
|
+
type: 'confirm',
|
|
334
|
+
name: 'overwrite',
|
|
335
|
+
message: `Profile "${profileName}" 已存在,是否覆盖?`,
|
|
336
|
+
default: false
|
|
337
|
+
}
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
if (!overwrite) {
|
|
341
|
+
console.log(chalk.yellow('已取消'));
|
|
342
|
+
process.exit(0);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fs.writeFileSync(profilePath, JSON.stringify(settings, null, 2));
|
|
347
|
+
console.log(chalk.green(`\n✓ Profile "${profileName}" 已保存到 ${profilePath}`));
|
|
348
|
+
|
|
349
|
+
// 如果是第一个 profile,设为默认
|
|
350
|
+
const profiles = getProfiles();
|
|
351
|
+
if (profiles.length === 1) {
|
|
352
|
+
setDefaultProfile(profileName);
|
|
353
|
+
console.log(chalk.green(`✓ 已设为默认 profile`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|