@pikecode/api-key-manager 1.1.3 → 1.2.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.
@@ -11,10 +11,10 @@ const { validator } = require('../utils/validator');
11
11
  const { Logger } = require('../utils/logger');
12
12
  const { UIHelper } = require('../utils/ui-helper');
13
13
  const { BaseCommand } = require('./BaseCommand');
14
- const {
15
- AUTH_MODE_DISPLAY_DETAILED,
16
- IDE_NAMES
17
- } = require('../constants');
14
+ const { promptProviderInfo, promptLaunchArgs, promptModelConfig, promptCodexLaunchArgs } = require('./add/prompts');
15
+ const { importCodexConfig } = require('./add/codexImporter');
16
+ const { saveProvider } = require('./add/providerSaver');
17
+ const { printProviderSummary } = require('./add/summaryPrinter');
18
18
 
19
19
  /**
20
20
  * 供应商添加器类
@@ -62,134 +62,7 @@ class ProviderAdder extends BaseCommand {
62
62
 
63
63
  async addCustomProvider() {
64
64
  try {
65
- const answers = await this.promptWithESC([
66
- {
67
- type: 'list',
68
- name: 'ideName',
69
- message: '选择要管理的 IDE:',
70
- choices: [
71
- { name: 'Claude Code (Anthropic)', value: 'claude' },
72
- { name: 'Codex CLI (OpenAI)', value: 'codex' }
73
- ],
74
- default: this.presetIdeName || 'claude',
75
- when: () => !this.presetIdeName
76
- },
77
- {
78
- type: 'list',
79
- name: 'importFromExisting',
80
- message: '是否从现有 Codex 配置导入?',
81
- choices: [
82
- { name: '从 ~/.codex 导入现有配置', value: 'import' },
83
- { name: '手动输入配置', value: 'manual' }
84
- ],
85
- default: 'import',
86
- when: (answers) => (answers.ideName || this.presetIdeName) === 'codex'
87
- },
88
- {
89
- type: 'input',
90
- name: 'name',
91
- message: '请输入供应商名称:',
92
- validate: (input) => {
93
- const error = validator.validateName(input);
94
- if (error) return error;
95
- return true;
96
- }
97
- },
98
- {
99
- type: 'list',
100
- name: 'authMode',
101
- message: '选择认证模式:',
102
- choices: [
103
- { name: '🔑 ANTHROPIC_API_KEY - 大多数第三方代理使用', value: 'api_key' },
104
- { name: '🔐 ANTHROPIC_AUTH_TOKEN - 部分服务商使用', value: 'auth_token' }
105
- ],
106
- default: 'api_key',
107
- when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
108
- },
109
- {
110
- type: 'input',
111
- name: 'baseUrl',
112
- message: '请输入 API 基础URL (ANTHROPIC_BASE_URL):',
113
- validate: (input) => {
114
- if (!input) return 'API 基础URL不能为空';
115
- const error = validator.validateUrl(input);
116
- if (error) return error;
117
- return true;
118
- },
119
- when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
120
- },
121
- {
122
- type: 'input',
123
- name: 'authToken',
124
- message: (answers) => {
125
- const envVar = answers.authMode === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
126
- return `请输入 Token (${envVar}):`;
127
- },
128
- validate: (input) => {
129
- const error = validator.validateToken(input);
130
- if (error) return error;
131
- return true;
132
- },
133
- when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
134
- },
135
- {
136
- type: 'input',
137
- name: 'baseUrl',
138
- message: '请输入 OpenAI API 基础URL (如使用官方API可留空):',
139
- default: '',
140
- validate: (input) => {
141
- if (!input) return true;
142
- const error = validator.validateUrl(input);
143
- if (error) return error;
144
- return true;
145
- },
146
- when: (answers) => (answers.ideName || this.presetIdeName) === 'codex' && answers.importFromExisting === 'manual'
147
- },
148
- {
149
- type: 'input',
150
- name: 'authToken',
151
- message: '请输入 OpenAI API Key (OPENAI_API_KEY):',
152
- validate: (input) => {
153
- if (!input) return 'API Key 不能为空';
154
- const error = validator.validateToken(input);
155
- if (error) return error;
156
- return true;
157
- },
158
- when: (answers) => (answers.ideName || this.presetIdeName) === 'codex' && answers.importFromExisting === 'manual'
159
- },
160
- {
161
- type: 'confirm',
162
- name: 'setAsDefault',
163
- message: '是否设置为当前供应商?',
164
- default: true
165
- },
166
- {
167
- type: 'confirm',
168
- name: 'configureLaunchArgs',
169
- message: '是否配置启动参数?',
170
- default: false,
171
- when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
172
- },
173
- {
174
- type: 'confirm',
175
- name: 'configureCodexLaunchArgs',
176
- message: '是否配置 Codex 启动参数?',
177
- default: false,
178
- when: (answers) => (answers.ideName || this.presetIdeName) === 'codex'
179
- },
180
- {
181
- type: 'confirm',
182
- name: 'configureModels',
183
- message: '是否配置模型参数?',
184
- default: false,
185
- when: (answers) => (answers.ideName || this.presetIdeName) !== 'codex'
186
- }
187
- ], '取消添加', () => {
188
- Logger.info('取消添加供应商');
189
- // 使用CommandRegistry避免循环引用
190
- const { registry } = require('../CommandRegistry');
191
- registry.executeCommand('switch');
192
- });
65
+ const answers = await promptProviderInfo(this);
193
66
 
194
67
  // 如果是预设的 ideName,设置到 answers 中
195
68
  if (!answers.ideName && this.presetIdeName) {
@@ -202,7 +75,7 @@ class ProviderAdder extends BaseCommand {
202
75
 
203
76
  // 从现有配置导入
204
77
  if (answers.importFromExisting === 'import') {
205
- const importedConfig = await this.importCodexConfig();
78
+ const importedConfig = await importCodexConfig();
206
79
  if (importedConfig) {
207
80
  answers.authToken = importedConfig.apiKey;
208
81
  answers.baseUrl = importedConfig.baseUrl;
@@ -229,62 +102,15 @@ class ProviderAdder extends BaseCommand {
229
102
 
230
103
  // Codex 启动参数配置
231
104
  if (answers.configureCodexLaunchArgs) {
232
- answers.launchArgs = await this.promptCodexLaunchArgsSelection();
233
- }
234
- }
235
-
236
- await this.saveProvider(answers);
237
- } catch (error) {
238
- if (this.isEscCancelled(error)) {
239
- return;
240
- }
241
- throw error;
242
- }
243
- }
244
-
245
- async saveProvider(answers) {
246
- try {
247
- await this.configManager.load();
248
-
249
- if (this.configManager.getProvider(answers.name)) {
250
- const shouldOverwrite = await this.confirmOverwrite(answers.name);
251
- if (!shouldOverwrite) {
252
- Logger.warning('操作已取消');
253
- return;
105
+ answers.launchArgs = await promptCodexLaunchArgs(this);
254
106
  }
255
107
  }
256
108
 
257
- const launchArgs = answers.ideName === 'codex'
258
- ? (Array.isArray(answers.launchArgs) ? answers.launchArgs : [])
259
- : (answers.configureLaunchArgs ? await this.promptLaunchArgsSelection() : []);
260
-
261
- const modelConfig = answers.configureModels
262
- ? await this.promptModelConfiguration()
263
- : { primaryModel: null, smallFastModel: null };
264
-
265
- await this.configManager.addProvider(answers.name, {
266
- displayName: answers.displayName || answers.name,
267
- ideName: answers.ideName || 'claude',
268
- baseUrl: answers.baseUrl,
269
- authToken: answers.authToken,
270
- authMode: answers.authMode,
271
- codexFiles: answers.codexFiles || null,
272
- launchArgs,
273
- primaryModel: modelConfig.primaryModel,
274
- smallFastModel: modelConfig.smallFastModel,
275
- setAsDefault: answers.setAsDefault
276
- });
277
-
278
- this.printProviderSummary(answers, launchArgs, modelConfig);
279
- await this.pauseBeforeReturn();
280
-
281
- const { registry } = require('../CommandRegistry');
282
- return await registry.executeCommand('switch');
109
+ await saveProvider(this, answers);
283
110
  } catch (error) {
284
111
  if (this.isEscCancelled(error)) {
285
112
  return;
286
113
  }
287
- Logger.error(`添加供应商失败: ${error.message}`);
288
114
  throw error;
289
115
  }
290
116
  }
@@ -300,8 +126,8 @@ class ProviderAdder extends BaseCommand {
300
126
  }
301
127
  ], '取消覆盖', () => {
302
128
  Logger.info('取消覆盖供应商');
303
- const { switchCommand } = require('./switch');
304
- switchCommand();
129
+ const { registry } = require('../CommandRegistry');
130
+ registry.executeCommand('switch');
305
131
  });
306
132
 
307
133
  return overwrite;
@@ -310,212 +136,6 @@ class ProviderAdder extends BaseCommand {
310
136
  }
311
137
  }
312
138
 
313
- async promptLaunchArgsSelection() {
314
- console.log(UIHelper.createTitle('配置启动参数', UIHelper.icons.settings));
315
- console.log();
316
- console.log(UIHelper.createTooltip('选择要使用的启动参数'));
317
- console.log();
318
- console.log(UIHelper.createStepIndicator(2, 2, '可选: 配置启动参数'));
319
- console.log(UIHelper.createHintLine([
320
- ['空格', '切换选中'],
321
- ['A', '全选'],
322
- ['I', '反选'],
323
- ['Enter', '确认选择'],
324
- ['ESC', '跳过配置']
325
- ]));
326
- console.log();
327
-
328
- const result = await this.promptWithESCAndDefault([
329
- {
330
- type: 'checkbox',
331
- name: 'launchArgs',
332
- message: '请选择启动参数:',
333
- choices: validator.getAvailableLaunchArgs().map(arg => ({
334
- name: `${arg.name} - ${arg.description}`,
335
- value: arg.name,
336
- checked: false
337
- }))
338
- }
339
- ], '跳过配置', () => {
340
- Logger.info('跳过启动参数配置');
341
- }, { launchArgs: [] });
342
-
343
- return result.launchArgs;
344
- }
345
-
346
- async promptModelConfiguration() {
347
- console.log(UIHelper.createTitle('配置模型参数', UIHelper.icons.settings));
348
- console.log();
349
- console.log(UIHelper.createTooltip('配置主模型和快速模型(可选)'));
350
- console.log();
351
- console.log(UIHelper.createStepIndicator(2, 2, '可选: 配置模型参数'));
352
- console.log(UIHelper.createHintLine([
353
- ['Enter', '确认输入'],
354
- ['ESC', '跳过配置']
355
- ]));
356
- console.log();
357
-
358
- const responses = await this.promptWithESCAndDefault([
359
- {
360
- type: 'input',
361
- name: 'primaryModel',
362
- message: '主模型 (ANTHROPIC_MODEL):',
363
- default: '',
364
- validate: (input) => {
365
- const error = validator.validateModel(input);
366
- if (error) return error;
367
- return true;
368
- }
369
- },
370
- {
371
- type: 'input',
372
- name: 'smallFastModel',
373
- message: '快速模型 (ANTHROPIC_SMALL_FAST_MODEL):',
374
- default: '',
375
- validate: (input) => {
376
- const error = validator.validateModel(input);
377
- if (error) return error;
378
- return true;
379
- }
380
- }
381
- ], '跳过配置', () => {
382
- Logger.info('跳过模型参数配置');
383
- }, { primaryModel: null, smallFastModel: null });
384
-
385
- return {
386
- primaryModel: responses.primaryModel,
387
- smallFastModel: responses.smallFastModel
388
- };
389
- }
390
-
391
- async importCodexConfig() {
392
- try {
393
- const { readCodexFiles, extractBaseUrlFromConfigToml } = require('../utils/codex-files');
394
- const codexFiles = await readCodexFiles();
395
-
396
- if (!codexFiles.authJson) {
397
- return null;
398
- }
399
-
400
- // 解析 auth.json 获取 API Key
401
- const authData = JSON.parse(codexFiles.authJson);
402
- const apiKey = authData.api_key || authData.openai_api_key || authData.OPENAI_API_KEY;
403
-
404
- if (!apiKey) {
405
- return null;
406
- }
407
-
408
- // 从 config.toml 中读取当前激活 provider 的 base_url
409
- // 优先从 [model_providers.<key>] section 读取,兼容旧的顶层 api_base_url 格式
410
- let baseUrl = null;
411
- if (codexFiles.configToml) {
412
- baseUrl = extractBaseUrlFromConfigToml(codexFiles.configToml);
413
- if (!baseUrl) {
414
- // 兼容旧格式(akm 之前错误写入的顶层字段)
415
- const legacyMatch = codexFiles.configToml.match(/^api_base_url\s*=\s*["']([^"']+)["']/m);
416
- if (legacyMatch) baseUrl = legacyMatch[1];
417
- }
418
- }
419
-
420
- Logger.success(`成功从 ${codexFiles.codexHome} 导入配置`);
421
- return { apiKey, baseUrl };
422
- } catch (error) {
423
- Logger.warning(`导入配置失败: ${error.message}`);
424
- return null;
425
- }
426
- }
427
-
428
- async promptCodexLaunchArgsSelection() {
429
- console.log(UIHelper.createTitle('配置 Codex 启动参数', UIHelper.icons.settings));
430
- console.log();
431
- console.log(UIHelper.createTooltip('选择要使用的 Codex 启动参数'));
432
- console.log();
433
- console.log(UIHelper.createHintLine([
434
- ['空格', '切换选中'],
435
- ['A', '全选'],
436
- ['I', '反选'],
437
- ['Enter', '确认选择'],
438
- ['ESC', '跳过配置']
439
- ]));
440
- console.log();
441
-
442
- try {
443
- const { getCodexLaunchArgs, checkExclusiveArgs } = require('../utils/launch-args');
444
- const codexArgs = getCodexLaunchArgs();
445
-
446
- const result = await this.promptWithESCAndDefault([
447
- {
448
- type: 'checkbox',
449
- name: 'launchArgs',
450
- message: '请选择 Codex 启动参数:',
451
- choices: codexArgs.map(arg => ({
452
- name: `${arg.label} (${arg.name}) - ${arg.description}`,
453
- value: arg.name,
454
- checked: arg.checked
455
- }))
456
- }
457
- ], '跳过配置', () => {
458
- Logger.info('跳过 Codex 启动参数配置');
459
- }, { launchArgs: [] });
460
-
461
- const { launchArgs } = result;
462
-
463
- const conflictError = checkExclusiveArgs(launchArgs, codexArgs);
464
- if (conflictError) {
465
- Logger.warning(conflictError);
466
- return await this.promptCodexLaunchArgsSelection();
467
- }
468
- return launchArgs;
469
- } catch (error) {
470
- throw error;
471
- }
472
- }
473
-
474
- printProviderSummary(answers, launchArgs, modelConfig) {
475
- const finalDisplayName = answers.displayName || answers.name;
476
- Logger.success(`供应商 '${finalDisplayName}' 添加成功!`);
477
-
478
- console.log(chalk.blue('\n配置详情:'));
479
- console.log(chalk.gray(` 名称: ${answers.name}`));
480
- console.log(chalk.gray(` 显示名称: ${finalDisplayName}`));
481
-
482
- if (answers.ideName === 'codex') {
483
- console.log(chalk.gray(' IDE: Codex CLI'));
484
- if (answers.baseUrl) {
485
- console.log(chalk.gray(` OPENAI_BASE_URL: ${answers.baseUrl}`));
486
- }
487
- if (answers.authToken) {
488
- console.log(chalk.gray(` OPENAI_API_KEY: ${answers.authToken}`));
489
- }
490
- if (answers.launchArgs && answers.launchArgs.length > 0) {
491
- console.log(chalk.gray(` 启动参数: ${answers.launchArgs.join(' ')}`));
492
- }
493
- console.log(chalk.green('\n🎉 供应商添加完成!正在返回主界面...'));
494
- return;
495
- }
496
-
497
- console.log(chalk.gray(` 认证模式: ${AUTH_MODE_DISPLAY_DETAILED[answers.authMode] || answers.authMode}`));
498
-
499
- if (answers.baseUrl) {
500
- console.log(chalk.gray(` 基础URL: ${answers.baseUrl}`));
501
- }
502
- console.log(chalk.gray(` Token: ${answers.authToken}`));
503
-
504
- if (launchArgs.length > 0) {
505
- console.log(chalk.gray(` 启动参数: ${launchArgs.join(' ')}`));
506
- }
507
-
508
- if (modelConfig.primaryModel) {
509
- console.log(chalk.gray(` 主模型: ${modelConfig.primaryModel}`));
510
- }
511
-
512
- if (modelConfig.smallFastModel) {
513
- console.log(chalk.gray(` 快速模型: ${modelConfig.smallFastModel}`));
514
- }
515
-
516
- console.log(chalk.green('\n🎉 供应商添加完成!正在返回主界面...'));
517
- }
518
-
519
139
  async pauseBeforeReturn(delay = 1500) {
520
140
  await new Promise(resolve => setTimeout(resolve, delay));
521
141
  }
@@ -99,7 +99,7 @@ class ProviderEditor extends BaseCommand {
99
99
  {
100
100
  type: 'input',
101
101
  name: 'displayName',
102
- message: '供应商显示名称:',
102
+ message: '供应商名称:',
103
103
  default: providerToEdit.displayName,
104
104
  validate: (input) => validator.validateDisplayName(input) || true
105
105
  }
@@ -90,24 +90,15 @@ class ProviderRemover extends BaseCommand {
90
90
 
91
91
  console.log(UIHelper.createTitle('删除供应商', UIHelper.icons.delete));
92
92
  console.log();
93
- console.log(UIHelper.createTooltip('选择要删除的供应商'));
93
+ console.log(UIHelper.createTooltip('选择要删除的供应商(可多选)'));
94
94
  console.log();
95
95
 
96
96
  const choices = providers.map(provider => ({
97
97
  name: `${provider.current ? '✅' : '🔹'} ${provider.name} (${provider.displayName})${provider.current ? ' - 当前使用中' : ''}`,
98
98
  value: provider.name,
99
- short: provider.name
99
+ checked: false
100
100
  }));
101
101
 
102
- choices.push(
103
- new inquirer.Separator(),
104
- { name: '❌ 取消删除', value: '__CANCEL__' }
105
- );
106
-
107
- // 对于删除操作,不默认选中当前供应商,而是选中第一个非当前的供应商
108
- const nonCurrentProvider = providers.find(p => !p.current);
109
- const defaultChoice = nonCurrentProvider ? nonCurrentProvider.name : providers[0]?.name;
110
-
111
102
  // 设置 ESC 键监听
112
103
  const escListener = this.createESCListener(() => {
113
104
  Logger.info('取消删除供应商');
@@ -121,12 +112,17 @@ class ProviderRemover extends BaseCommand {
121
112
  try {
122
113
  answer = await this.prompt([
123
114
  {
124
- type: 'list',
125
- name: 'provider',
126
- message: '选择要删除的供应商:',
115
+ type: 'checkbox',
116
+ name: 'providers',
117
+ message: '选择要删除的供应商(空格选择,Enter确认):',
127
118
  choices,
128
- default: defaultChoice,
129
- pageSize: 10
119
+ pageSize: 10,
120
+ validate: (selected) => {
121
+ if (selected.length === 0) {
122
+ return '请至少选择一个供应商';
123
+ }
124
+ return true;
125
+ }
130
126
  }
131
127
  ]);
132
128
  } catch (error) {
@@ -139,12 +135,61 @@ class ProviderRemover extends BaseCommand {
139
135
 
140
136
  this.removeESCListener(escListener);
141
137
 
142
- if (answer.provider === '__CANCEL__') {
138
+ if (!answer.providers || answer.providers.length === 0) {
143
139
  Logger.info('删除操作已取消');
144
140
  return;
145
141
  }
146
142
 
147
- await this.remove(answer.provider);
143
+ // 显示将要删除的供应商列表
144
+ console.log();
145
+ console.log(UIHelper.createTitle(`即将删除 ${answer.providers.length} 个供应商`, '⚠️'));
146
+ answer.providers.forEach(name => {
147
+ const provider = this.configManager.getProvider(name);
148
+ console.log(` • ${provider.displayName} (${name})`);
149
+ });
150
+ console.log();
151
+
152
+ // 最终确认
153
+ let finalConfirm;
154
+ try {
155
+ finalConfirm = await this.prompt([
156
+ {
157
+ type: 'confirm',
158
+ name: 'confirm',
159
+ message: `确定要删除这 ${answer.providers.length} 个供应商吗?`,
160
+ default: false
161
+ }
162
+ ]);
163
+ } catch (error) {
164
+ if (this.isEscCancelled(error)) {
165
+ return;
166
+ }
167
+ throw error;
168
+ }
169
+
170
+ if (!finalConfirm.confirm) {
171
+ Logger.warning('删除操作已取消');
172
+ return;
173
+ }
174
+
175
+ // 批量删除
176
+ let successCount = 0;
177
+ let failCount = 0;
178
+ for (const providerName of answer.providers) {
179
+ try {
180
+ const provider = this.configManager.getProvider(providerName);
181
+ await this.configManager.removeProvider(providerName);
182
+ Logger.success(`✓ 已删除: ${provider.displayName}`);
183
+ successCount++;
184
+ } catch (error) {
185
+ Logger.error(`✗ 删除失败: ${providerName} - ${error.message}`);
186
+ failCount++;
187
+ }
188
+ }
189
+
190
+ console.log();
191
+ Logger.success(`删除完成: 成功 ${successCount} 个${failCount > 0 ? `, 失败 ${failCount} 个` : ''}`);
192
+
148
193
  } catch (error) {
149
194
  this.removeESCListener(escListener);
150
195
  throw error;