@pikecode/api-key-manager 1.0.20 → 1.0.23
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/akm.js +27 -4
- package/package.json +6 -1
- package/src/commands/add.js +233 -10
- package/src/commands/current.js +25 -2
- package/src/commands/edit.js +115 -62
- package/src/commands/list.js +63 -34
- package/src/commands/switch.js +49 -13
- package/src/utils/codex-files.js +120 -0
- package/src/utils/codex-launcher.js +121 -0
- package/src/utils/provider-status-checker.js +50 -0
package/bin/akm.js
CHANGED
|
@@ -34,15 +34,35 @@ program
|
|
|
34
34
|
program
|
|
35
35
|
.command('add')
|
|
36
36
|
.description('添加新的API密钥配置')
|
|
37
|
-
.
|
|
37
|
+
.option('--codex', '直接添加 Codex CLI 供应商')
|
|
38
|
+
.option('--claude', '直接添加 Claude Code 供应商')
|
|
39
|
+
.action(async (options) => {
|
|
38
40
|
try {
|
|
39
|
-
|
|
41
|
+
const ideName = options.codex ? 'codex' : (options.claude ? 'claude' : null);
|
|
42
|
+
await registry.executeCommand('add', { ideName });
|
|
40
43
|
} catch (error) {
|
|
41
44
|
console.error(chalk.red('❌ 添加失败:'), error.message);
|
|
42
45
|
process.exit(1);
|
|
43
46
|
}
|
|
44
47
|
});
|
|
45
48
|
|
|
49
|
+
// Switch command
|
|
50
|
+
program
|
|
51
|
+
.command('switch')
|
|
52
|
+
.description('切换到指定供应商')
|
|
53
|
+
.argument('[provider]', '直接切换到指定供应商')
|
|
54
|
+
.option('--codex', '仅显示 Codex CLI 供应商')
|
|
55
|
+
.option('--claude', '仅显示 Claude Code 供应商')
|
|
56
|
+
.action(async (provider, options) => {
|
|
57
|
+
try {
|
|
58
|
+
const filter = options.codex ? 'codex' : (options.claude ? 'claude' : null);
|
|
59
|
+
await registry.executeCommand('switch', provider, { filter });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(chalk.red('❌ 切换失败:'), error.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
46
66
|
// Remove command
|
|
47
67
|
program
|
|
48
68
|
.command('remove')
|
|
@@ -61,9 +81,12 @@ program
|
|
|
61
81
|
program
|
|
62
82
|
.command('list')
|
|
63
83
|
.description('列出所有API密钥配置')
|
|
64
|
-
.
|
|
84
|
+
.option('--codex', '仅显示 Codex CLI 供应商')
|
|
85
|
+
.option('--claude', '仅显示 Claude Code 供应商')
|
|
86
|
+
.action(async (options) => {
|
|
65
87
|
try {
|
|
66
|
-
|
|
88
|
+
const filter = options.codex ? 'codex' : (options.claude ? 'claude' : null);
|
|
89
|
+
await registry.executeCommand('list', filter);
|
|
67
90
|
} catch (error) {
|
|
68
91
|
console.error(chalk.red('❌ 列表失败:'), error.message);
|
|
69
92
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikecode/api-key-manager",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "A CLI tool for managing and switching multiple API provider configurations",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"anthropic",
|
|
30
30
|
"anthropic-api-key",
|
|
31
31
|
"anthropic-auth-token",
|
|
32
|
+
"openai",
|
|
33
|
+
"openai-api-key",
|
|
34
|
+
"codex",
|
|
35
|
+
"codex-cli",
|
|
36
|
+
"claude-code",
|
|
32
37
|
"environment",
|
|
33
38
|
"config",
|
|
34
39
|
"provider",
|
package/src/commands/add.js
CHANGED
|
@@ -7,9 +7,10 @@ const { UIHelper } = require('../utils/ui-helper');
|
|
|
7
7
|
const { BaseCommand } = require('./BaseCommand');
|
|
8
8
|
|
|
9
9
|
class ProviderAdder extends BaseCommand {
|
|
10
|
-
constructor() {
|
|
10
|
+
constructor(options = {}) {
|
|
11
11
|
super();
|
|
12
12
|
this.configManager = new ConfigManager();
|
|
13
|
+
this.presetIdeName = options.ideName || null;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
async interactive() {
|
|
@@ -174,6 +175,28 @@ class ProviderAdder extends BaseCommand {
|
|
|
174
175
|
|
|
175
176
|
try {
|
|
176
177
|
const answers = await this.prompt([
|
|
178
|
+
{
|
|
179
|
+
type: 'list',
|
|
180
|
+
name: 'ideName',
|
|
181
|
+
message: '选择要管理的 IDE:',
|
|
182
|
+
choices: [
|
|
183
|
+
{ name: 'Claude Code (Anthropic)', value: 'claude' },
|
|
184
|
+
{ name: 'Codex CLI (OpenAI)', value: 'codex' }
|
|
185
|
+
],
|
|
186
|
+
default: this.presetIdeName || 'claude',
|
|
187
|
+
when: () => !this.presetIdeName
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'list',
|
|
191
|
+
name: 'importFromExisting',
|
|
192
|
+
message: '是否从现有 Codex 配置导入?',
|
|
193
|
+
choices: [
|
|
194
|
+
{ name: '从 ~/.codex 导入现有配置', value: 'import' },
|
|
195
|
+
{ name: '手动输入配置', value: 'manual' }
|
|
196
|
+
],
|
|
197
|
+
default: 'import',
|
|
198
|
+
when: (answers) => (answers.ideName || this.presetIdeName) === 'codex'
|
|
199
|
+
},
|
|
177
200
|
{
|
|
178
201
|
type: 'input',
|
|
179
202
|
name: 'name',
|
|
@@ -203,7 +226,8 @@ class ProviderAdder extends BaseCommand {
|
|
|
203
226
|
{ name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
|
|
204
227
|
{ name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' }
|
|
205
228
|
],
|
|
206
|
-
default: 'api_key'
|
|
229
|
+
default: 'api_key',
|
|
230
|
+
when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
|
|
207
231
|
},
|
|
208
232
|
{
|
|
209
233
|
type: 'list',
|
|
@@ -214,7 +238,7 @@ class ProviderAdder extends BaseCommand {
|
|
|
214
238
|
{ name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
|
|
215
239
|
],
|
|
216
240
|
default: 'api_key',
|
|
217
|
-
when: (answers) => answers.authMode === 'api_key'
|
|
241
|
+
when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex' && answers.authMode === 'api_key'
|
|
218
242
|
},
|
|
219
243
|
{
|
|
220
244
|
type: 'input',
|
|
@@ -238,7 +262,7 @@ class ProviderAdder extends BaseCommand {
|
|
|
238
262
|
if (error) return error;
|
|
239
263
|
return true;
|
|
240
264
|
},
|
|
241
|
-
when: (answers) => answers.authMode === 'api_key' || answers.authMode === 'auth_token'
|
|
265
|
+
when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex' && (answers.authMode === 'api_key' || answers.authMode === 'auth_token')
|
|
242
266
|
},
|
|
243
267
|
{
|
|
244
268
|
type: 'input',
|
|
@@ -260,7 +284,33 @@ class ProviderAdder extends BaseCommand {
|
|
|
260
284
|
const error = validator.validateToken(input);
|
|
261
285
|
if (error) return error;
|
|
262
286
|
return true;
|
|
263
|
-
}
|
|
287
|
+
},
|
|
288
|
+
when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
type: 'input',
|
|
292
|
+
name: 'baseUrl',
|
|
293
|
+
message: '请输入 OpenAI API 基础URL (如使用官方API可留空):',
|
|
294
|
+
default: '',
|
|
295
|
+
validate: (input) => {
|
|
296
|
+
if (!input) return true;
|
|
297
|
+
const error = validator.validateUrl(input);
|
|
298
|
+
if (error) return error;
|
|
299
|
+
return true;
|
|
300
|
+
},
|
|
301
|
+
when: (answers) => (answers.ideName || this.presetIdeName) === 'codex' && answers.importFromExisting === 'manual'
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
type: 'input',
|
|
305
|
+
name: 'authToken',
|
|
306
|
+
message: '请输入 OpenAI API Key (OPENAI_API_KEY):',
|
|
307
|
+
validate: (input) => {
|
|
308
|
+
if (!input) return 'API Key 不能为空';
|
|
309
|
+
const error = validator.validateToken(input);
|
|
310
|
+
if (error) return error;
|
|
311
|
+
return true;
|
|
312
|
+
},
|
|
313
|
+
when: (answers) => (answers.ideName || this.presetIdeName) === 'codex' && answers.importFromExisting === 'manual'
|
|
264
314
|
},
|
|
265
315
|
{
|
|
266
316
|
type: 'confirm',
|
|
@@ -272,19 +322,71 @@ class ProviderAdder extends BaseCommand {
|
|
|
272
322
|
type: 'confirm',
|
|
273
323
|
name: 'configureLaunchArgs',
|
|
274
324
|
message: '是否配置启动参数?',
|
|
275
|
-
default: false
|
|
325
|
+
default: false,
|
|
326
|
+
when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
type: 'confirm',
|
|
330
|
+
name: 'configureCodexLaunchArgs',
|
|
331
|
+
message: '是否配置 Codex 启动参数?',
|
|
332
|
+
default: false,
|
|
333
|
+
when: (answers) => (answers.ideName || this.presetIdeName) === 'codex'
|
|
276
334
|
},
|
|
277
335
|
{
|
|
278
336
|
type: 'confirm',
|
|
279
337
|
name: 'configureModels',
|
|
280
338
|
message: '是否配置模型参数?',
|
|
281
|
-
default: false
|
|
339
|
+
default: false,
|
|
340
|
+
when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
|
|
282
341
|
}
|
|
283
342
|
]);
|
|
284
343
|
|
|
285
344
|
// 移除 ESC 键监听
|
|
286
345
|
this.removeESCListener(escListener);
|
|
287
|
-
|
|
346
|
+
|
|
347
|
+
// 如果是预设的 ideName,设置到 answers 中
|
|
348
|
+
if (!answers.ideName && this.presetIdeName) {
|
|
349
|
+
answers.ideName = this.presetIdeName;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (answers.ideName === 'codex') {
|
|
353
|
+
answers.authMode = 'openai_api_key';
|
|
354
|
+
answers.tokenType = null;
|
|
355
|
+
answers.codexFiles = null;
|
|
356
|
+
|
|
357
|
+
// 从现有配置导入
|
|
358
|
+
if (answers.importFromExisting === 'import') {
|
|
359
|
+
const importedConfig = await this.importCodexConfig();
|
|
360
|
+
if (importedConfig) {
|
|
361
|
+
answers.authToken = importedConfig.apiKey;
|
|
362
|
+
answers.baseUrl = importedConfig.baseUrl;
|
|
363
|
+
} else {
|
|
364
|
+
Logger.warning('未能导入现有配置,请手动输入');
|
|
365
|
+
const manualAnswers = await this.prompt([
|
|
366
|
+
{
|
|
367
|
+
type: 'input',
|
|
368
|
+
name: 'baseUrl',
|
|
369
|
+
message: '请输入 OpenAI API 基础URL (如使用官方API可留空):',
|
|
370
|
+
default: ''
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
type: 'input',
|
|
374
|
+
name: 'authToken',
|
|
375
|
+
message: '请输入 OpenAI API Key (OPENAI_API_KEY):',
|
|
376
|
+
validate: (input) => input ? true : 'API Key 不能为空'
|
|
377
|
+
}
|
|
378
|
+
]);
|
|
379
|
+
answers.authToken = manualAnswers.authToken;
|
|
380
|
+
answers.baseUrl = manualAnswers.baseUrl;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Codex 启动参数配置
|
|
385
|
+
if (answers.configureCodexLaunchArgs) {
|
|
386
|
+
answers.launchArgs = await this.promptCodexLaunchArgsSelection();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
288
390
|
await this.saveProvider(answers);
|
|
289
391
|
} catch (error) {
|
|
290
392
|
// 移除 ESC 键监听
|
|
@@ -315,10 +417,12 @@ class ProviderAdder extends BaseCommand {
|
|
|
315
417
|
|
|
316
418
|
await this.configManager.addProvider(answers.name, {
|
|
317
419
|
displayName: answers.displayName || answers.name,
|
|
420
|
+
ideName: answers.ideName || 'claude',
|
|
318
421
|
baseUrl: answers.baseUrl,
|
|
319
422
|
authToken: answers.authToken,
|
|
320
423
|
authMode: answers.authMode,
|
|
321
424
|
tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
|
|
425
|
+
codexFiles: answers.codexFiles || null,
|
|
322
426
|
launchArgs,
|
|
323
427
|
primaryModel: modelConfig.primaryModel,
|
|
324
428
|
smallFastModel: modelConfig.smallFastModel,
|
|
@@ -464,6 +568,110 @@ class ProviderAdder extends BaseCommand {
|
|
|
464
568
|
}
|
|
465
569
|
}
|
|
466
570
|
|
|
571
|
+
async importCodexConfig() {
|
|
572
|
+
try {
|
|
573
|
+
const { readCodexFiles } = require('../utils/codex-files');
|
|
574
|
+
const codexFiles = await readCodexFiles();
|
|
575
|
+
|
|
576
|
+
if (!codexFiles.authJson) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 解析 auth.json 获取 API Key
|
|
581
|
+
const authData = JSON.parse(codexFiles.authJson);
|
|
582
|
+
const apiKey = authData.api_key || authData.openai_api_key || authData.OPENAI_API_KEY;
|
|
583
|
+
|
|
584
|
+
if (!apiKey) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 尝试从 config.toml 获取 base URL
|
|
589
|
+
let baseUrl = null;
|
|
590
|
+
if (codexFiles.configToml) {
|
|
591
|
+
const baseUrlMatch = codexFiles.configToml.match(/api_base\s*=\s*["']([^"']+)["']/);
|
|
592
|
+
if (baseUrlMatch) {
|
|
593
|
+
baseUrl = baseUrlMatch[1];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
Logger.success(`成功从 ${codexFiles.codexHome} 导入配置`);
|
|
598
|
+
return { apiKey, baseUrl };
|
|
599
|
+
} catch (error) {
|
|
600
|
+
Logger.warning(`导入配置失败: ${error.message}`);
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async promptCodexLaunchArgsSelection() {
|
|
606
|
+
console.log(UIHelper.createTitle('配置 Codex 启动参数', UIHelper.icons.settings));
|
|
607
|
+
console.log();
|
|
608
|
+
console.log(UIHelper.createTooltip('选择要使用的 Codex 启动参数'));
|
|
609
|
+
console.log();
|
|
610
|
+
console.log(UIHelper.createHintLine([
|
|
611
|
+
['空格', '切换选中'],
|
|
612
|
+
['A', '全选'],
|
|
613
|
+
['I', '反选'],
|
|
614
|
+
['Enter', '确认选择'],
|
|
615
|
+
['ESC', '跳过配置']
|
|
616
|
+
]));
|
|
617
|
+
console.log();
|
|
618
|
+
|
|
619
|
+
const escListener = this.createESCListener(() => {
|
|
620
|
+
Logger.info('跳过 Codex 启动参数配置');
|
|
621
|
+
}, '跳过配置');
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const codexArgs = [
|
|
625
|
+
{
|
|
626
|
+
name: '--full-auto',
|
|
627
|
+
label: '全自动模式',
|
|
628
|
+
description: '自动批准所有操作',
|
|
629
|
+
checked: false
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: '--dangerously-bypass-approvals-and-sandbox',
|
|
633
|
+
label: '跳过审批和沙盒',
|
|
634
|
+
description: '危险:跳过所有安全检查',
|
|
635
|
+
checked: false
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: '--model',
|
|
639
|
+
label: '指定模型',
|
|
640
|
+
description: '使用特定模型 (需手动指定)',
|
|
641
|
+
checked: false
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: '--quiet',
|
|
645
|
+
label: '静默模式',
|
|
646
|
+
description: '减少输出信息',
|
|
647
|
+
checked: false
|
|
648
|
+
}
|
|
649
|
+
];
|
|
650
|
+
|
|
651
|
+
const { launchArgs } = await this.prompt([
|
|
652
|
+
{
|
|
653
|
+
type: 'checkbox',
|
|
654
|
+
name: 'launchArgs',
|
|
655
|
+
message: '请选择 Codex 启动参数:',
|
|
656
|
+
choices: codexArgs.map(arg => ({
|
|
657
|
+
name: `${arg.label} (${arg.name}) - ${arg.description}`,
|
|
658
|
+
value: arg.name,
|
|
659
|
+
checked: arg.checked
|
|
660
|
+
}))
|
|
661
|
+
}
|
|
662
|
+
]);
|
|
663
|
+
|
|
664
|
+
this.removeESCListener(escListener);
|
|
665
|
+
return launchArgs;
|
|
666
|
+
} catch (error) {
|
|
667
|
+
this.removeESCListener(escListener);
|
|
668
|
+
if (this.isEscCancelled(error)) {
|
|
669
|
+
return [];
|
|
670
|
+
}
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
467
675
|
printProviderSummary(answers, launchArgs, modelConfig) {
|
|
468
676
|
const finalDisplayName = answers.displayName || answers.name;
|
|
469
677
|
Logger.success(`供应商 '${finalDisplayName}' 添加成功!`);
|
|
@@ -472,6 +680,21 @@ class ProviderAdder extends BaseCommand {
|
|
|
472
680
|
console.log(chalk.gray(` 名称: ${answers.name}`));
|
|
473
681
|
console.log(chalk.gray(` 显示名称: ${finalDisplayName}`));
|
|
474
682
|
|
|
683
|
+
if (answers.ideName === 'codex') {
|
|
684
|
+
console.log(chalk.gray(' IDE: Codex CLI'));
|
|
685
|
+
if (answers.baseUrl) {
|
|
686
|
+
console.log(chalk.gray(` OPENAI_BASE_URL: ${answers.baseUrl}`));
|
|
687
|
+
}
|
|
688
|
+
if (answers.authToken) {
|
|
689
|
+
console.log(chalk.gray(` OPENAI_API_KEY: ${answers.authToken}`));
|
|
690
|
+
}
|
|
691
|
+
if (answers.launchArgs && answers.launchArgs.length > 0) {
|
|
692
|
+
console.log(chalk.gray(` 启动参数: ${answers.launchArgs.join(' ')}`));
|
|
693
|
+
}
|
|
694
|
+
console.log(chalk.green('\n🎉 供应商添加完成!正在返回主界面...'));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
475
698
|
const authModeDisplay = {
|
|
476
699
|
api_key: '通用API密钥模式',
|
|
477
700
|
auth_token: '认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN)',
|
|
@@ -511,8 +734,8 @@ class ProviderAdder extends BaseCommand {
|
|
|
511
734
|
}
|
|
512
735
|
}
|
|
513
736
|
|
|
514
|
-
async function addCommand() {
|
|
515
|
-
const adder = new ProviderAdder();
|
|
737
|
+
async function addCommand(options = {}) {
|
|
738
|
+
const adder = new ProviderAdder(options);
|
|
516
739
|
try {
|
|
517
740
|
await adder.interactive();
|
|
518
741
|
} catch (error) {
|
package/src/commands/current.js
CHANGED
|
@@ -14,7 +14,7 @@ class CurrentConfig {
|
|
|
14
14
|
|
|
15
15
|
if (!currentProvider) {
|
|
16
16
|
Logger.warning('未设置当前供应商');
|
|
17
|
-
Logger.info('请使用 "
|
|
17
|
+
Logger.info('请使用 "akm <供应商名>" 切换供应商');
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -24,6 +24,29 @@ class CurrentConfig {
|
|
|
24
24
|
console.log(chalk.green(`供应商: ${currentProvider.displayName}`));
|
|
25
25
|
console.log(chalk.gray(`内部名称: ${currentProvider.name}`));
|
|
26
26
|
|
|
27
|
+
if (currentProvider.ideName === 'codex') {
|
|
28
|
+
console.log(chalk.gray('IDE: Codex CLI'));
|
|
29
|
+
if (currentProvider.baseUrl) {
|
|
30
|
+
console.log(chalk.gray(`OPENAI_BASE_URL: ${currentProvider.baseUrl}`));
|
|
31
|
+
}
|
|
32
|
+
if (currentProvider.authToken) {
|
|
33
|
+
console.log(chalk.gray(`OPENAI_API_KEY: ${currentProvider.authToken}`));
|
|
34
|
+
}
|
|
35
|
+
console.log(chalk.gray(`创建时间: ${new Date(currentProvider.createdAt).toLocaleString()}`));
|
|
36
|
+
console.log(chalk.gray(`最后使用: ${new Date(currentProvider.lastUsed).toLocaleString()}`));
|
|
37
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
38
|
+
|
|
39
|
+
console.log(chalk.blue('\n🔧 环境变量设置:'));
|
|
40
|
+
if (currentProvider.baseUrl) {
|
|
41
|
+
console.log(chalk.gray(`set OPENAI_BASE_URL=${currentProvider.baseUrl}`));
|
|
42
|
+
}
|
|
43
|
+
if (currentProvider.authToken) {
|
|
44
|
+
console.log(chalk.gray(`set OPENAI_API_KEY=${currentProvider.authToken}`));
|
|
45
|
+
}
|
|
46
|
+
console.log(chalk.gray('codex'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
27
50
|
// 显示认证模式
|
|
28
51
|
const authModeDisplay = {
|
|
29
52
|
api_key: '通用API密钥模式',
|
|
@@ -91,4 +114,4 @@ async function currentCommand() {
|
|
|
91
114
|
await current.show();
|
|
92
115
|
}
|
|
93
116
|
|
|
94
|
-
module.exports = { currentCommand, CurrentConfig };
|
|
117
|
+
module.exports = { currentCommand, CurrentConfig };
|
package/src/commands/edit.js
CHANGED
|
@@ -64,6 +64,8 @@ class ProviderEditor extends BaseCommand {
|
|
|
64
64
|
console.log(UIHelper.createTooltip('请更新供应商配置信息。按 Enter 键接受默认值。'));
|
|
65
65
|
console.log();
|
|
66
66
|
|
|
67
|
+
const isCodex = providerToEdit.ideName === 'codex';
|
|
68
|
+
|
|
67
69
|
const escListener = this.createESCListener(() => {
|
|
68
70
|
Logger.info('取消编辑供应商。');
|
|
69
71
|
const { registry } = require('../CommandRegistry');
|
|
@@ -73,61 +75,88 @@ class ProviderEditor extends BaseCommand {
|
|
|
73
75
|
try {
|
|
74
76
|
let answers;
|
|
75
77
|
try {
|
|
76
|
-
|
|
78
|
+
const questions = [
|
|
77
79
|
{
|
|
78
80
|
type: 'input',
|
|
79
81
|
name: 'displayName',
|
|
80
82
|
message: '供应商显示名称:',
|
|
81
83
|
default: providerToEdit.displayName,
|
|
82
84
|
validate: (input) => validator.validateDisplayName(input) || true,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
type: 'list',
|
|
97
|
-
name: 'tokenType',
|
|
98
|
-
message: 'Token类型:',
|
|
99
|
-
choices: [
|
|
100
|
-
{ name: '🔑 ANTHROPIC_API_KEY - 通用API密钥', value: 'api_key' },
|
|
101
|
-
{ name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
|
|
102
|
-
],
|
|
103
|
-
default: providerToEdit.tokenType || 'api_key',
|
|
104
|
-
when: (answers) => answers.authMode === 'api_key'
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
type: 'input',
|
|
108
|
-
name: 'baseUrl',
|
|
109
|
-
message: 'API基础URL:',
|
|
110
|
-
default: providerToEdit.baseUrl,
|
|
111
|
-
validate: (input) => validator.validateUrl(input) || true,
|
|
112
|
-
when: (answers) => answers.authMode === 'api_key' || answers.authMode === 'auth_token',
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
type: 'input',
|
|
116
|
-
name: 'authToken',
|
|
117
|
-
message: (answers) => {
|
|
118
|
-
switch (answers.authMode) {
|
|
119
|
-
case 'api_key':
|
|
120
|
-
const tokenTypeLabel = answers.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
|
|
121
|
-
return `Token (${tokenTypeLabel}):`;
|
|
122
|
-
case 'auth_token': return '认证令牌 (ANTHROPIC_AUTH_TOKEN):';
|
|
123
|
-
case 'oauth_token': return 'OAuth令牌 (CLAUDE_CODE_OAUTH_TOKEN):';
|
|
124
|
-
default: return '认证令牌:';
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
if (isCodex) {
|
|
89
|
+
questions.push(
|
|
90
|
+
{
|
|
91
|
+
type: 'input',
|
|
92
|
+
name: 'baseUrl',
|
|
93
|
+
message: 'OpenAI API 基础URL (留空使用官方API):',
|
|
94
|
+
default: providerToEdit.baseUrl || '',
|
|
95
|
+
validate: (input) => {
|
|
96
|
+
if (!input) return true;
|
|
97
|
+
return validator.validateUrl(input) || true;
|
|
125
98
|
}
|
|
126
99
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
100
|
+
{
|
|
101
|
+
type: 'input',
|
|
102
|
+
name: 'authToken',
|
|
103
|
+
message: 'OpenAI API Key (OPENAI_API_KEY):',
|
|
104
|
+
default: providerToEdit.authToken,
|
|
105
|
+
validate: (input) => {
|
|
106
|
+
if (!input) return 'API Key 不能为空';
|
|
107
|
+
return validator.validateToken(input) || true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
questions.push(
|
|
113
|
+
{
|
|
114
|
+
type: 'list',
|
|
115
|
+
name: 'authMode',
|
|
116
|
+
message: '认证模式:',
|
|
117
|
+
choices: [
|
|
118
|
+
{ name: '🔑 通用API密钥模式 - 支持 ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN', value: 'api_key' },
|
|
119
|
+
{ name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
|
|
120
|
+
{ name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' },
|
|
121
|
+
],
|
|
122
|
+
default: providerToEdit.authMode,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'list',
|
|
126
|
+
name: 'tokenType',
|
|
127
|
+
message: 'Token类型:',
|
|
128
|
+
choices: [
|
|
129
|
+
{ name: '🔑 ANTHROPIC_API_KEY - 通用API密钥', value: 'api_key' },
|
|
130
|
+
{ name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
|
|
131
|
+
],
|
|
132
|
+
default: providerToEdit.tokenType || 'api_key',
|
|
133
|
+
when: (answers) => answers.authMode === 'api_key'
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'input',
|
|
137
|
+
name: 'baseUrl',
|
|
138
|
+
message: 'API基础URL:',
|
|
139
|
+
default: providerToEdit.baseUrl,
|
|
140
|
+
validate: (input) => validator.validateUrl(input) || true,
|
|
141
|
+
when: (answers) => answers.authMode === 'api_key' || answers.authMode === 'auth_token',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: 'input',
|
|
145
|
+
name: 'authToken',
|
|
146
|
+
message: (answers) => {
|
|
147
|
+
switch (answers.authMode) {
|
|
148
|
+
case 'api_key':
|
|
149
|
+
const tokenTypeLabel = answers.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
|
|
150
|
+
return `Token (${tokenTypeLabel}):`;
|
|
151
|
+
case 'auth_token': return '认证令牌 (ANTHROPIC_AUTH_TOKEN):';
|
|
152
|
+
case 'oauth_token': return 'OAuth令牌 (CLAUDE_CODE_OAUTH_TOKEN):';
|
|
153
|
+
default: return '认证令牌:';
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
default: providerToEdit.authToken,
|
|
157
|
+
validate: (input) => validator.validateToken(input) || true,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
131
160
|
type: 'checkbox',
|
|
132
161
|
name: 'launchArgs',
|
|
133
162
|
message: '启动参数:',
|
|
@@ -136,8 +165,11 @@ class ProviderEditor extends BaseCommand {
|
|
|
136
165
|
value: arg.name,
|
|
137
166
|
checked: providerToEdit.launchArgs && providerToEdit.launchArgs.includes(arg.name),
|
|
138
167
|
})),
|
|
139
|
-
|
|
140
|
-
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
answers = await this.prompt(questions);
|
|
141
173
|
} catch (error) {
|
|
142
174
|
this.removeESCListener(escListener);
|
|
143
175
|
if (this.isEscCancelled(error)) {
|
|
@@ -147,6 +179,7 @@ class ProviderEditor extends BaseCommand {
|
|
|
147
179
|
}
|
|
148
180
|
|
|
149
181
|
this.removeESCListener(escListener);
|
|
182
|
+
|
|
150
183
|
await this.saveProvider(providerToEdit.name, answers);
|
|
151
184
|
|
|
152
185
|
} catch (error) {
|
|
@@ -160,19 +193,39 @@ class ProviderEditor extends BaseCommand {
|
|
|
160
193
|
|
|
161
194
|
async saveProvider(name, answers) {
|
|
162
195
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
196
|
+
const existingProvider = this.configManager.getProvider(name);
|
|
197
|
+
const ideName = existingProvider?.ideName || 'claude';
|
|
198
|
+
|
|
199
|
+
if (ideName === 'codex') {
|
|
200
|
+
await this.configManager.addProvider(name, {
|
|
201
|
+
displayName: answers.displayName,
|
|
202
|
+
ideName: 'codex',
|
|
203
|
+
authMode: 'openai_api_key',
|
|
204
|
+
baseUrl: answers.baseUrl || null,
|
|
205
|
+
authToken: answers.authToken,
|
|
206
|
+
tokenType: null,
|
|
207
|
+
codexFiles: null,
|
|
208
|
+
launchArgs: existingProvider.launchArgs || [],
|
|
209
|
+
primaryModel: existingProvider.models?.primary || null,
|
|
210
|
+
smallFastModel: existingProvider.models?.smallFast || null,
|
|
211
|
+
setAsDefault: false
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
// Re-use addProvider which can overwrite existing providers
|
|
215
|
+
await this.configManager.addProvider(name, {
|
|
216
|
+
displayName: answers.displayName,
|
|
217
|
+
ideName,
|
|
218
|
+
baseUrl: answers.baseUrl,
|
|
219
|
+
authToken: answers.authToken,
|
|
220
|
+
authMode: answers.authMode,
|
|
221
|
+
tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
|
|
222
|
+
launchArgs: answers.launchArgs,
|
|
223
|
+
// Retain original model settings unless we add editing for them
|
|
224
|
+
primaryModel: existingProvider.models.primary,
|
|
225
|
+
smallFastModel: existingProvider.models.smallFast,
|
|
226
|
+
setAsDefault: false, // Don't change default status on edit
|
|
227
|
+
});
|
|
228
|
+
}
|
|
176
229
|
|
|
177
230
|
Logger.success(`供应商 '${answers.displayName}' 更新成功!`);
|
|
178
231
|
|
package/src/commands/list.js
CHANGED
|
@@ -9,20 +9,34 @@ class ProviderLister {
|
|
|
9
9
|
this.statusChecker = new ProviderStatusChecker();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async list() {
|
|
12
|
+
async list(filter = null) {
|
|
13
13
|
try {
|
|
14
14
|
await this.configManager.ensureLoaded();
|
|
15
|
-
|
|
15
|
+
let providers = this.configManager.listProviders();
|
|
16
16
|
const currentProvider = this.configManager.getCurrentProvider();
|
|
17
|
+
|
|
18
|
+
// 应用过滤器
|
|
19
|
+
if (filter === 'codex') {
|
|
20
|
+
providers = providers.filter(p => p.ideName === 'codex');
|
|
21
|
+
} else if (filter === 'claude') {
|
|
22
|
+
providers = providers.filter(p => p.ideName !== 'codex');
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
const statusMap = await this.statusChecker.checkAll(providers);
|
|
18
26
|
|
|
19
27
|
if (providers.length === 0) {
|
|
20
|
-
|
|
28
|
+
if (filter) {
|
|
29
|
+
const filterName = filter === 'codex' ? 'Codex CLI' : 'Claude Code';
|
|
30
|
+
Logger.warning(`暂无 ${filterName} 供应商配置`);
|
|
31
|
+
} else {
|
|
32
|
+
Logger.warning('暂无配置的供应商');
|
|
33
|
+
}
|
|
21
34
|
Logger.info('请使用 "akm add" 添加供应商配置');
|
|
22
35
|
return;
|
|
23
36
|
}
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
const titleSuffix = filter === 'codex' ? ' (Codex CLI)' : (filter === 'claude' ? ' (Claude Code)' : '');
|
|
39
|
+
console.log(chalk.blue(`\n📋 供应商列表${titleSuffix}:`));
|
|
26
40
|
console.log(chalk.gray('═'.repeat(60)));
|
|
27
41
|
|
|
28
42
|
providers.forEach((provider, index) => {
|
|
@@ -33,41 +47,56 @@ class ProviderLister {
|
|
|
33
47
|
const availabilityText = this._formatAvailability(availability);
|
|
34
48
|
const nameColor = isCurrent ? chalk.green : chalk.white;
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
// IDE 类型标签
|
|
51
|
+
const ideTag = provider.ideName === 'codex'
|
|
52
|
+
? chalk.cyan('[Codex]')
|
|
53
|
+
: chalk.magenta('[Claude]');
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
const authModeDisplay = {
|
|
40
|
-
api_key: '通用API密钥模式',
|
|
41
|
-
auth_token: '认证令牌模式',
|
|
42
|
-
oauth_token: 'OAuth令牌模式'
|
|
43
|
-
};
|
|
44
|
-
console.log(chalk.gray(` 认证模式: ${authModeDisplay[provider.authMode] || provider.authMode}`));
|
|
55
|
+
console.log(`${status} ${availabilityIcon} ${ideTag} ${nameColor(provider.name)} (${provider.displayName}) - ${availabilityText}`);
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// OAuth 模式
|
|
49
|
-
if (provider.authToken) {
|
|
50
|
-
console.log(chalk.gray(` CLAUDE_CODE_OAUTH_TOKEN: ${provider.authToken}`));
|
|
51
|
-
}
|
|
52
|
-
if (provider.baseUrl) {
|
|
53
|
-
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
|
|
54
|
-
}
|
|
55
|
-
} else if (provider.authMode === 'api_key') {
|
|
56
|
-
// API Key 模式
|
|
57
|
+
if (provider.ideName === 'codex') {
|
|
58
|
+
console.log(chalk.gray(' IDE: Codex CLI'));
|
|
57
59
|
if (provider.baseUrl) {
|
|
58
|
-
console.log(chalk.gray(`
|
|
60
|
+
console.log(chalk.gray(` OPENAI_BASE_URL: ${provider.baseUrl}`));
|
|
59
61
|
}
|
|
60
62
|
if (provider.authToken) {
|
|
61
|
-
|
|
62
|
-
console.log(chalk.gray(` ${tokenEnvName}: ${provider.authToken}`));
|
|
63
|
+
console.log(chalk.gray(` OPENAI_API_KEY: ${provider.authToken}`));
|
|
63
64
|
}
|
|
64
65
|
} else {
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
// 显示认证模式
|
|
67
|
+
const authModeDisplay = {
|
|
68
|
+
api_key: '通用API密钥模式',
|
|
69
|
+
auth_token: '认证令牌模式',
|
|
70
|
+
oauth_token: 'OAuth令牌模式'
|
|
71
|
+
};
|
|
72
|
+
console.log(chalk.gray(` 认证模式: ${authModeDisplay[provider.authMode] || provider.authMode}`));
|
|
73
|
+
|
|
74
|
+
// 根据不同模式显示对应的环境变量名称
|
|
75
|
+
if (provider.authMode === 'oauth_token') {
|
|
76
|
+
// OAuth 模式
|
|
77
|
+
if (provider.authToken) {
|
|
78
|
+
console.log(chalk.gray(` CLAUDE_CODE_OAUTH_TOKEN: ${provider.authToken}`));
|
|
79
|
+
}
|
|
80
|
+
if (provider.baseUrl) {
|
|
81
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
|
|
82
|
+
}
|
|
83
|
+
} else if (provider.authMode === 'api_key') {
|
|
84
|
+
// API Key 模式
|
|
85
|
+
if (provider.baseUrl) {
|
|
86
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
|
|
87
|
+
}
|
|
88
|
+
if (provider.authToken) {
|
|
89
|
+
const tokenEnvName = provider.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
|
|
90
|
+
console.log(chalk.gray(` ${tokenEnvName}: ${provider.authToken}`));
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// auth_token 模式
|
|
94
|
+
if (provider.baseUrl) {
|
|
95
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
|
|
96
|
+
}
|
|
97
|
+
if (provider.authToken) {
|
|
98
|
+
console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${provider.authToken}`));
|
|
99
|
+
}
|
|
71
100
|
}
|
|
72
101
|
}
|
|
73
102
|
|
|
@@ -132,9 +161,9 @@ class ProviderLister {
|
|
|
132
161
|
}
|
|
133
162
|
}
|
|
134
163
|
|
|
135
|
-
async function listCommand() {
|
|
164
|
+
async function listCommand(filter = null) {
|
|
136
165
|
const lister = new ProviderLister();
|
|
137
|
-
await lister.list();
|
|
166
|
+
await lister.list(filter);
|
|
138
167
|
}
|
|
139
168
|
|
|
140
169
|
module.exports = { listCommand, ProviderLister };
|
package/src/commands/switch.js
CHANGED
|
@@ -4,6 +4,7 @@ const chalk = require('chalk');
|
|
|
4
4
|
const Choices = require('inquirer/lib/objects/choices');
|
|
5
5
|
const { ConfigManager } = require('../config');
|
|
6
6
|
const { executeWithEnv } = require('../utils/env-launcher');
|
|
7
|
+
const { executeCodexWithEnv } = require('../utils/codex-launcher');
|
|
7
8
|
const { Logger } = require('../utils/logger');
|
|
8
9
|
const { UIHelper } = require('../utils/ui-helper');
|
|
9
10
|
const { findSettingsConflict, backupSettingsFile, clearConflictKeys, saveSettingsFile } = require('../utils/claude-settings');
|
|
@@ -19,6 +20,7 @@ class EnvSwitcher extends BaseCommand {
|
|
|
19
20
|
this.latestStatusMap = {};
|
|
20
21
|
this.currentPromptContext = null;
|
|
21
22
|
this.activeStatusRefresh = null;
|
|
23
|
+
this.filter = null;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
async validateProvider(providerName) {
|
|
@@ -34,6 +36,10 @@ class EnvSwitcher extends BaseCommand {
|
|
|
34
36
|
try {
|
|
35
37
|
this.clearScreen();
|
|
36
38
|
const provider = await this.validateProvider(providerName);
|
|
39
|
+
if (provider.ideName === 'codex') {
|
|
40
|
+
// Codex CLI 使用环境变量注入方式启动,直接跳过参数选择
|
|
41
|
+
return await this.launchProvider(provider, provider.launchArgs || []);
|
|
42
|
+
}
|
|
37
43
|
const availableArgs = this.getAvailableLaunchArgs();
|
|
38
44
|
|
|
39
45
|
console.log(UIHelper.createTitle('启动配置', UIHelper.icons.launch));
|
|
@@ -198,15 +204,20 @@ class EnvSwitcher extends BaseCommand {
|
|
|
198
204
|
|
|
199
205
|
async launchProvider(provider, selectedLaunchArgs) {
|
|
200
206
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
const isCodex = provider.ideName === 'codex';
|
|
208
|
+
|
|
209
|
+
// Claude Code 才需要检测设置冲突
|
|
210
|
+
if (!isCodex) {
|
|
211
|
+
const shouldContinue = await this.ensureClaudeSettingsCompatibility(provider);
|
|
212
|
+
if (!shouldContinue) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
205
215
|
}
|
|
206
216
|
|
|
207
217
|
this.clearScreen();
|
|
208
218
|
console.log(UIHelper.createTitle('正在启动', UIHelper.icons.loading));
|
|
209
219
|
console.log();
|
|
220
|
+
const ideDisplayName = isCodex ? 'Codex CLI' : 'Claude Code';
|
|
210
221
|
console.log(UIHelper.createCard('目标供应商', UIHelper.formatProvider(provider), UIHelper.icons.launch));
|
|
211
222
|
|
|
212
223
|
if (selectedLaunchArgs.length > 0) {
|
|
@@ -228,11 +239,15 @@ class EnvSwitcher extends BaseCommand {
|
|
|
228
239
|
|
|
229
240
|
UIHelper.clearLoadingAnimation(loadingInterval);
|
|
230
241
|
|
|
231
|
-
console.log(UIHelper.createCard('准备就绪',
|
|
242
|
+
console.log(UIHelper.createCard('准备就绪', `环境配置完成,正在启动 🚀 ${ideDisplayName}...`, UIHelper.icons.success));
|
|
232
243
|
console.log();
|
|
233
244
|
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
if (isCodex) {
|
|
246
|
+
await executeCodexWithEnv(provider, selectedLaunchArgs);
|
|
247
|
+
} else {
|
|
248
|
+
// 设置环境变量并启动 Claude Code
|
|
249
|
+
await executeWithEnv(provider, selectedLaunchArgs);
|
|
250
|
+
}
|
|
236
251
|
|
|
237
252
|
} catch (error) {
|
|
238
253
|
UIHelper.clearLoadingAnimation(loadingInterval);
|
|
@@ -266,14 +281,26 @@ class EnvSwitcher extends BaseCommand {
|
|
|
266
281
|
async showProviderSelection() {
|
|
267
282
|
try {
|
|
268
283
|
// 并行加载配置和准备界面
|
|
269
|
-
|
|
284
|
+
let providers = await this.configManager.ensureLoaded().then(() => this.configManager.listProviders());
|
|
285
|
+
|
|
286
|
+
// 应用过滤器
|
|
287
|
+
if (this.filter === 'codex') {
|
|
288
|
+
providers = providers.filter(p => p.ideName === 'codex');
|
|
289
|
+
} else if (this.filter === 'claude') {
|
|
290
|
+
providers = providers.filter(p => p.ideName !== 'codex');
|
|
291
|
+
}
|
|
270
292
|
|
|
271
293
|
const initialStatusMap = this._buildInitialStatusMap(providers);
|
|
272
294
|
// 显示欢迎界面(立即渲染)
|
|
273
295
|
this.showWelcomeScreen(providers, initialStatusMap, null);
|
|
274
296
|
|
|
275
297
|
if (providers.length === 0) {
|
|
276
|
-
|
|
298
|
+
if (this.filter) {
|
|
299
|
+
const filterName = this.filter === 'codex' ? 'Codex CLI' : 'Claude Code';
|
|
300
|
+
Logger.warning(`暂无 ${filterName} 供应商配置`);
|
|
301
|
+
} else {
|
|
302
|
+
Logger.warning('暂无配置的供应商');
|
|
303
|
+
}
|
|
277
304
|
Logger.info('请先运行 "akm add" 添加供应商配置');
|
|
278
305
|
return;
|
|
279
306
|
}
|
|
@@ -300,6 +327,10 @@ class EnvSwitcher extends BaseCommand {
|
|
|
300
327
|
const currentProvider = providers.find(p => p.current);
|
|
301
328
|
const defaultChoice = currentProvider ? currentProvider.name : providers[0]?.name;
|
|
302
329
|
|
|
330
|
+
// 构建提示信息
|
|
331
|
+
const filterSuffix = this.filter === 'codex' ? ' (Codex CLI)' : (this.filter === 'claude' ? ' (Claude Code)' : '');
|
|
332
|
+
const promptMessage = `请选择要切换的供应商${filterSuffix} (总计 ${providers.length} 个):`;
|
|
333
|
+
|
|
303
334
|
// 设置 ESC 键监听
|
|
304
335
|
const escListener = this.createESCListener(() => {
|
|
305
336
|
Logger.info('退出程序');
|
|
@@ -311,7 +342,7 @@ class EnvSwitcher extends BaseCommand {
|
|
|
311
342
|
{
|
|
312
343
|
type: 'list',
|
|
313
344
|
name: 'provider',
|
|
314
|
-
message:
|
|
345
|
+
message: promptMessage,
|
|
315
346
|
choices,
|
|
316
347
|
default: defaultChoice,
|
|
317
348
|
pageSize: 12
|
|
@@ -849,7 +880,11 @@ class EnvSwitcher extends BaseCommand {
|
|
|
849
880
|
const icon = this._iconForState(availability?.state);
|
|
850
881
|
const statusText = this._formatAvailability(availability);
|
|
851
882
|
const statusLabel = chalk.gray('-') + ' ' + statusText;
|
|
852
|
-
|
|
883
|
+
// IDE 类型标签
|
|
884
|
+
const ideTag = provider.ideName === 'codex'
|
|
885
|
+
? chalk.cyan('[Codex]')
|
|
886
|
+
: chalk.magenta('[Claude]');
|
|
887
|
+
const label = `${icon} ${ideTag} ${UIHelper.formatProvider(provider)}${isLastUsed ? UIHelper.colors.muted(' --- 上次使用') : ''} ${statusLabel}`;
|
|
853
888
|
|
|
854
889
|
return {
|
|
855
890
|
name: label,
|
|
@@ -1451,9 +1486,10 @@ class EnvSwitcher extends BaseCommand {
|
|
|
1451
1486
|
}
|
|
1452
1487
|
}
|
|
1453
1488
|
|
|
1454
|
-
async function switchCommand(providerName) {
|
|
1489
|
+
async function switchCommand(providerName, options = {}) {
|
|
1455
1490
|
const switcher = new EnvSwitcher();
|
|
1456
|
-
|
|
1491
|
+
switcher.filter = options.filter || null;
|
|
1492
|
+
|
|
1457
1493
|
try {
|
|
1458
1494
|
if (providerName) {
|
|
1459
1495
|
await switcher.showLaunchArgsSelection(providerName);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CODEX_DIR_NAME = '.codex';
|
|
6
|
+
const CONFIG_TOML_FILE = 'config.toml';
|
|
7
|
+
const AUTH_JSON_FILE = 'auth.json';
|
|
8
|
+
const BACKUP_DIR_NAME = 'akm-backups';
|
|
9
|
+
|
|
10
|
+
function resolveCodexHome() {
|
|
11
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), CODEX_DIR_NAME);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildCodexPaths(codexHome = resolveCodexHome()) {
|
|
15
|
+
return {
|
|
16
|
+
codexHome,
|
|
17
|
+
configTomlPath: path.join(codexHome, CONFIG_TOML_FILE),
|
|
18
|
+
authJsonPath: path.join(codexHome, AUTH_JSON_FILE)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timestampSuffix() {
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
25
|
+
return [
|
|
26
|
+
now.getFullYear(),
|
|
27
|
+
pad(now.getMonth() + 1),
|
|
28
|
+
pad(now.getDate()),
|
|
29
|
+
'_',
|
|
30
|
+
pad(now.getHours()),
|
|
31
|
+
pad(now.getMinutes()),
|
|
32
|
+
pad(now.getSeconds())
|
|
33
|
+
].join('');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function ensureCodexHome(codexHome = resolveCodexHome()) {
|
|
37
|
+
await fs.ensureDir(codexHome);
|
|
38
|
+
return codexHome;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function setSecurePermissions(filePath) {
|
|
42
|
+
if (process.platform === 'win32') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await fs.chmod(filePath, 0o600);
|
|
47
|
+
} catch {
|
|
48
|
+
// 忽略权限设置失败
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readCodexFiles(codexHome = resolveCodexHome()) {
|
|
53
|
+
const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
|
|
54
|
+
const configToml = await fs.pathExists(configTomlPath)
|
|
55
|
+
? await fs.readFile(configTomlPath, 'utf8')
|
|
56
|
+
: null;
|
|
57
|
+
const authJson = await fs.pathExists(authJsonPath)
|
|
58
|
+
? await fs.readFile(authJsonPath, 'utf8')
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
codexHome,
|
|
63
|
+
configToml,
|
|
64
|
+
authJson
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function backupCodexFiles(codexHome = resolveCodexHome()) {
|
|
69
|
+
const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
|
|
70
|
+
const hasConfig = await fs.pathExists(configTomlPath);
|
|
71
|
+
const hasAuth = await fs.pathExists(authJsonPath);
|
|
72
|
+
if (!hasConfig && !hasAuth) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const backupRoot = path.join(codexHome, BACKUP_DIR_NAME);
|
|
77
|
+
const backupDir = path.join(backupRoot, `backup-${timestampSuffix()}`);
|
|
78
|
+
await fs.ensureDir(backupDir);
|
|
79
|
+
|
|
80
|
+
if (hasConfig) {
|
|
81
|
+
await fs.copy(configTomlPath, path.join(backupDir, CONFIG_TOML_FILE));
|
|
82
|
+
}
|
|
83
|
+
if (hasAuth) {
|
|
84
|
+
await fs.copy(authJsonPath, path.join(backupDir, AUTH_JSON_FILE));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return backupDir;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function applyCodexProfile(profile, options = {}) {
|
|
91
|
+
if (!profile || (profile.configToml == null && profile.authJson == null)) {
|
|
92
|
+
throw new Error('Codex 配置为空,无法切换');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const codexHome = await ensureCodexHome(profile.codexHome || options.codexHome);
|
|
96
|
+
const { configTomlPath, authJsonPath } = buildCodexPaths(codexHome);
|
|
97
|
+
|
|
98
|
+
const backupDir = await backupCodexFiles(codexHome);
|
|
99
|
+
|
|
100
|
+
if (profile.configToml != null) {
|
|
101
|
+
await fs.writeFile(configTomlPath, profile.configToml, 'utf8');
|
|
102
|
+
await setSecurePermissions(configTomlPath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (profile.authJson != null) {
|
|
106
|
+
await fs.writeFile(authJsonPath, profile.authJson, 'utf8');
|
|
107
|
+
await setSecurePermissions(authJsonPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { codexHome, backupDir };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
resolveCodexHome,
|
|
115
|
+
buildCodexPaths,
|
|
116
|
+
readCodexFiles,
|
|
117
|
+
applyCodexProfile,
|
|
118
|
+
backupCodexFiles
|
|
119
|
+
};
|
|
120
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const spawn = require('cross-spawn');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 清理环境变量值,移除危险字符
|
|
5
|
+
* @param {string} value - 要清理的值
|
|
6
|
+
* @returns {string} 清理后的值
|
|
7
|
+
*/
|
|
8
|
+
function sanitizeEnvValue(value) {
|
|
9
|
+
if (typeof value !== 'string') {
|
|
10
|
+
throw new Error('环境变量值必须是字符串');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 移除控制字符
|
|
14
|
+
let cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
15
|
+
|
|
16
|
+
// 检测可能的 shell 命令注入
|
|
17
|
+
if (/[;&|`$()]/.test(cleaned)) {
|
|
18
|
+
throw new Error('环境变量值包含潜在不安全的字符');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return cleaned;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function clearTerminal() {
|
|
25
|
+
if (!process.stdout || typeof process.stdout.write !== 'function') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
process.stdout.write('\x1bc');
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// 某些终端可能不支持 RIS 序列,忽略即可
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sequence = process.platform === 'win32'
|
|
36
|
+
? '\x1b[3J\x1b[2J\x1b[0f'
|
|
37
|
+
: '\x1b[3J\x1b[2J\x1b[H';
|
|
38
|
+
try {
|
|
39
|
+
process.stdout.write(sequence);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// 忽略清屏失败
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 构建 Codex CLI 环境变量
|
|
47
|
+
* @param {object} config - 供应商配置
|
|
48
|
+
* @returns {object} 环境变量对象
|
|
49
|
+
*/
|
|
50
|
+
function buildCodexEnvVariables(config) {
|
|
51
|
+
const env = { ...process.env };
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Codex CLI 使用 OpenAI 环境变量
|
|
55
|
+
if (config.authToken) {
|
|
56
|
+
env.OPENAI_API_KEY = sanitizeEnvValue(config.authToken);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (config.baseUrl) {
|
|
60
|
+
env.OPENAI_BASE_URL = sanitizeEnvValue(config.baseUrl);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 支持自定义模型
|
|
64
|
+
if (config.models && config.models.primary) {
|
|
65
|
+
env.OPENAI_MODEL = sanitizeEnvValue(config.models.primary);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return env;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error(`配置验证失败: ${error.message}\n请使用 'akm edit ${config.name}' 修复配置`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 使用环境变量注入方式执行 Codex CLI
|
|
76
|
+
* @param {object} config - 供应商配置
|
|
77
|
+
* @param {string[]} launchArgs - 启动参数
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
async function executeCodexWithEnv(config, launchArgs = []) {
|
|
81
|
+
if (!config || config.ideName !== 'codex') {
|
|
82
|
+
throw new Error('无效的 Codex 供应商配置');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!config.authToken) {
|
|
86
|
+
throw new Error(`供应商 '${config.name}' 未配置 API Key,请使用 'akm edit ${config.name}' 添加`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const env = buildCodexEnvVariables(config);
|
|
90
|
+
const args = Array.isArray(launchArgs) ? [...launchArgs] : [];
|
|
91
|
+
|
|
92
|
+
clearTerminal();
|
|
93
|
+
|
|
94
|
+
console.log('\n启动 Codex CLI...\n');
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const child = spawn('codex', args, {
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
env,
|
|
100
|
+
shell: true
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.on('close', (code) => {
|
|
104
|
+
if (code === 0) {
|
|
105
|
+
resolve();
|
|
106
|
+
} else {
|
|
107
|
+
reject(new Error(`Codex CLI 退出,退出代码: ${code}\n提示: 请检查 API 配置是否正确`));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.on('error', (error) => {
|
|
112
|
+
if (error.code === 'ENOENT') {
|
|
113
|
+
reject(new Error('找不到 codex 命令\n请先安装 Codex CLI: npm i -g @openai/codex 或 brew install --cask codex'));
|
|
114
|
+
} else {
|
|
115
|
+
reject(new Error(`启动 Codex CLI 失败: ${error.message}`));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { executeCodexWithEnv, buildCodexEnvVariables };
|
|
@@ -13,6 +13,10 @@ class ProviderStatusChecker {
|
|
|
13
13
|
return this._result('unknown', '未找到配置', null);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
if (provider.ideName === 'codex') {
|
|
17
|
+
return this._checkCodex(provider);
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
if (provider.authMode === 'oauth_token') {
|
|
17
21
|
return this._result('unknown', '暂不支持 OAuth 令牌检测', null);
|
|
18
22
|
}
|
|
@@ -246,6 +250,52 @@ class ProviderStatusChecker {
|
|
|
246
250
|
_result(state, label, latency) {
|
|
247
251
|
return { state, label, latency };
|
|
248
252
|
}
|
|
253
|
+
|
|
254
|
+
async _checkCodex(provider) {
|
|
255
|
+
if (!provider.authToken) {
|
|
256
|
+
return this._result('unknown', '未配置 API Key', null);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const baseUrl = provider.baseUrl || 'https://api.openai.com/v1';
|
|
260
|
+
const modelsUrl = `${baseUrl.replace(/\/$/, '')}/models`;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const controller = new AbortController();
|
|
264
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
265
|
+
|
|
266
|
+
const start = process.hrtime.bigint();
|
|
267
|
+
const response = await fetch(modelsUrl, {
|
|
268
|
+
method: 'GET',
|
|
269
|
+
headers: {
|
|
270
|
+
'Authorization': `Bearer ${provider.authToken}`,
|
|
271
|
+
'Content-Type': 'application/json'
|
|
272
|
+
},
|
|
273
|
+
signal: controller.signal
|
|
274
|
+
});
|
|
275
|
+
const latency = Number(process.hrtime.bigint() - start) / 1e6;
|
|
276
|
+
|
|
277
|
+
clearTimeout(timeoutId);
|
|
278
|
+
|
|
279
|
+
if (response.ok) {
|
|
280
|
+
return this._result('online', `可用 ${latency.toFixed(0)}ms`, latency);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (response.status === 401 || response.status === 403) {
|
|
284
|
+
return this._result('offline', `认证失败 (${response.status})`, null);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (response.status >= 500) {
|
|
288
|
+
return this._result('degraded', `服务异常 (${response.status})`, null);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return this._result('offline', `请求失败 (${response.status})`, null);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error.name === 'AbortError') {
|
|
294
|
+
return this._result('offline', '请求超时', null);
|
|
295
|
+
}
|
|
296
|
+
return this._result('offline', `检测失败: ${error.message}`, null);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
249
299
|
}
|
|
250
300
|
|
|
251
301
|
module.exports = { ProviderStatusChecker };
|