@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 CHANGED
@@ -34,15 +34,35 @@ program
34
34
  program
35
35
  .command('add')
36
36
  .description('添加新的API密钥配置')
37
- .action(async () => {
37
+ .option('--codex', '直接添加 Codex CLI 供应商')
38
+ .option('--claude', '直接添加 Claude Code 供应商')
39
+ .action(async (options) => {
38
40
  try {
39
- await registry.executeCommand('add');
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
- .action(async () => {
84
+ .option('--codex', '仅显示 Codex CLI 供应商')
85
+ .option('--claude', '仅显示 Claude Code 供应商')
86
+ .action(async (options) => {
65
87
  try {
66
- await registry.executeCommand('list');
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.20",
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",
@@ -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) {
@@ -14,7 +14,7 @@ class CurrentConfig {
14
14
 
15
15
  if (!currentProvider) {
16
16
  Logger.warning('未设置当前供应商');
17
- Logger.info('请使用 "cc <供应商名>" 切换供应商');
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 };
@@ -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
- answers = await this.prompt([
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
- type: 'list',
86
- name: 'authMode',
87
- message: '认证模式:',
88
- choices: [
89
- { name: '🔑 通用API密钥模式 - 支持 ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN', value: 'api_key' },
90
- { name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
91
- { name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' },
92
- ],
93
- default: providerToEdit.authMode,
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
- default: providerToEdit.authToken,
128
- validate: (input) => validator.validateToken(input) || true,
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
- // Re-use addProvider which can overwrite existing providers
164
- await this.configManager.addProvider(name, {
165
- displayName: answers.displayName,
166
- baseUrl: answers.baseUrl,
167
- authToken: answers.authToken,
168
- authMode: answers.authMode,
169
- tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
170
- launchArgs: answers.launchArgs,
171
- // Retain original model settings unless we add editing for them
172
- primaryModel: this.configManager.getProvider(name).models.primary,
173
- smallFastModel: this.configManager.getProvider(name).models.smallFast,
174
- setAsDefault: false, // Don't change default status on edit
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
 
@@ -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
- const providers = this.configManager.listProviders();
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
- Logger.warning('暂无配置的供应商');
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
- console.log(chalk.blue('\n📋 供应商列表:'));
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
- console.log(`${status} ${availabilityIcon} ${nameColor(provider.name)} (${provider.displayName}) - ${availabilityText}`);
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
- if (provider.authMode === 'oauth_token') {
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(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
60
+ console.log(chalk.gray(` OPENAI_BASE_URL: ${provider.baseUrl}`));
59
61
  }
60
62
  if (provider.authToken) {
61
- const tokenEnvName = provider.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
62
- console.log(chalk.gray(` ${tokenEnvName}: ${provider.authToken}`));
63
+ console.log(chalk.gray(` OPENAI_API_KEY: ${provider.authToken}`));
63
64
  }
64
65
  } else {
65
- // auth_token 模式
66
- if (provider.baseUrl) {
67
- console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
68
- }
69
- if (provider.authToken) {
70
- console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${provider.authToken}`));
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 };
@@ -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
- // 执行 Claude Code 设置兼容性检查
202
- const shouldContinue = await this.ensureClaudeSettingsCompatibility(provider);
203
- if (!shouldContinue) {
204
- return;
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('准备就绪', '环境配置完成,正在启动 🚀 Claude Code...', UIHelper.icons.success));
242
+ console.log(UIHelper.createCard('准备就绪', `环境配置完成,正在启动 🚀 ${ideDisplayName}...`, UIHelper.icons.success));
232
243
  console.log();
233
244
 
234
- // 设置环境变量并启动 Claude Code
235
- await executeWithEnv(provider, selectedLaunchArgs);
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
- const providers = await this.configManager.ensureLoaded().then(() => this.configManager.listProviders());
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
- Logger.warning('暂无配置的供应商');
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: `请选择要切换的供应商 (总计 ${providers.length} 个):`,
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
- const label = `${icon} ${UIHelper.formatProvider(provider)}${isLastUsed ? UIHelper.colors.muted(' --- 上次使用') : ''} ${statusLabel}`;
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 };