@pikecode/api-key-manager 1.0.20 → 1.0.21
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 +5 -2
- package/package.json +1 -1
- package/src/commands/add.js +65 -5
- package/src/commands/current.js +25 -2
- package/src/commands/edit.js +115 -62
- package/src/commands/list.js +58 -34
- package/src/commands/switch.js +21 -7
- 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
|
@@ -61,9 +61,12 @@ program
|
|
|
61
61
|
program
|
|
62
62
|
.command('list')
|
|
63
63
|
.description('列出所有API密钥配置')
|
|
64
|
-
.
|
|
64
|
+
.option('--codex', '仅显示 Codex CLI 供应商')
|
|
65
|
+
.option('--claude', '仅显示 Claude Code 供应商')
|
|
66
|
+
.action(async (options) => {
|
|
65
67
|
try {
|
|
66
|
-
|
|
68
|
+
const filter = options.codex ? 'codex' : (options.claude ? 'claude' : null);
|
|
69
|
+
await registry.executeCommand('list', filter);
|
|
67
70
|
} catch (error) {
|
|
68
71
|
console.error(chalk.red('❌ 列表失败:'), error.message);
|
|
69
72
|
process.exit(1);
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -194,6 +194,16 @@ class ProviderAdder extends BaseCommand {
|
|
|
194
194
|
return true;
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
|
+
{
|
|
198
|
+
type: 'list',
|
|
199
|
+
name: 'ideName',
|
|
200
|
+
message: '选择要管理的 IDE:',
|
|
201
|
+
choices: [
|
|
202
|
+
{ name: 'Claude Code (Anthropic)', value: 'claude' },
|
|
203
|
+
{ name: 'Codex CLI (OpenAI)', value: 'codex' }
|
|
204
|
+
],
|
|
205
|
+
default: 'claude'
|
|
206
|
+
},
|
|
197
207
|
{
|
|
198
208
|
type: 'list',
|
|
199
209
|
name: 'authMode',
|
|
@@ -203,7 +213,8 @@ class ProviderAdder extends BaseCommand {
|
|
|
203
213
|
{ name: '🔐 认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN) - 适用于某些服务商', value: 'auth_token' },
|
|
204
214
|
{ name: '🌐 OAuth令牌模式 (CLAUDE_CODE_OAUTH_TOKEN) - 适用于官方Claude Code', value: 'oauth_token' }
|
|
205
215
|
],
|
|
206
|
-
default: 'api_key'
|
|
216
|
+
default: 'api_key',
|
|
217
|
+
when: (answers) => answers.ideName !== 'codex'
|
|
207
218
|
},
|
|
208
219
|
{
|
|
209
220
|
type: 'list',
|
|
@@ -214,7 +225,7 @@ class ProviderAdder extends BaseCommand {
|
|
|
214
225
|
{ name: '🔐 ANTHROPIC_AUTH_TOKEN - 认证令牌', value: 'auth_token' }
|
|
215
226
|
],
|
|
216
227
|
default: 'api_key',
|
|
217
|
-
when: (answers) => answers.authMode === 'api_key'
|
|
228
|
+
when: (answers) => answers.ideName !== 'codex' && answers.authMode === 'api_key'
|
|
218
229
|
},
|
|
219
230
|
{
|
|
220
231
|
type: 'input',
|
|
@@ -238,7 +249,7 @@ class ProviderAdder extends BaseCommand {
|
|
|
238
249
|
if (error) return error;
|
|
239
250
|
return true;
|
|
240
251
|
},
|
|
241
|
-
when: (answers) => answers.authMode === 'api_key' || answers.authMode === 'auth_token'
|
|
252
|
+
when: (answers) => answers.ideName !== 'codex' && (answers.authMode === 'api_key' || answers.authMode === 'auth_token')
|
|
242
253
|
},
|
|
243
254
|
{
|
|
244
255
|
type: 'input',
|
|
@@ -261,6 +272,33 @@ class ProviderAdder extends BaseCommand {
|
|
|
261
272
|
if (error) return error;
|
|
262
273
|
return true;
|
|
263
274
|
}
|
|
275
|
+
,
|
|
276
|
+
when: (answers) => answers.ideName !== 'codex'
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: 'input',
|
|
280
|
+
name: 'baseUrl',
|
|
281
|
+
message: '请输入 OpenAI API 基础URL (如使用官方API可留空):',
|
|
282
|
+
default: '',
|
|
283
|
+
validate: (input) => {
|
|
284
|
+
if (!input) return true;
|
|
285
|
+
const error = validator.validateUrl(input);
|
|
286
|
+
if (error) return error;
|
|
287
|
+
return true;
|
|
288
|
+
},
|
|
289
|
+
when: (answers) => answers.ideName === 'codex'
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: 'input',
|
|
293
|
+
name: 'authToken',
|
|
294
|
+
message: '请输入 OpenAI API Key (OPENAI_API_KEY):',
|
|
295
|
+
validate: (input) => {
|
|
296
|
+
if (!input) return 'API Key 不能为空';
|
|
297
|
+
const error = validator.validateToken(input);
|
|
298
|
+
if (error) return error;
|
|
299
|
+
return true;
|
|
300
|
+
},
|
|
301
|
+
when: (answers) => answers.ideName === 'codex'
|
|
264
302
|
},
|
|
265
303
|
{
|
|
266
304
|
type: 'confirm',
|
|
@@ -272,19 +310,27 @@ class ProviderAdder extends BaseCommand {
|
|
|
272
310
|
type: 'confirm',
|
|
273
311
|
name: 'configureLaunchArgs',
|
|
274
312
|
message: '是否配置启动参数?',
|
|
275
|
-
default: false
|
|
313
|
+
default: false,
|
|
314
|
+
when: (answers) => answers.ideName !== 'codex'
|
|
276
315
|
},
|
|
277
316
|
{
|
|
278
317
|
type: 'confirm',
|
|
279
318
|
name: 'configureModels',
|
|
280
319
|
message: '是否配置模型参数?',
|
|
281
|
-
default: false
|
|
320
|
+
default: false,
|
|
321
|
+
when: (answers) => answers.ideName !== 'codex'
|
|
282
322
|
}
|
|
283
323
|
]);
|
|
284
324
|
|
|
285
325
|
// 移除 ESC 键监听
|
|
286
326
|
this.removeESCListener(escListener);
|
|
287
327
|
|
|
328
|
+
if (answers.ideName === 'codex') {
|
|
329
|
+
answers.authMode = 'openai_api_key';
|
|
330
|
+
answers.tokenType = null;
|
|
331
|
+
answers.codexFiles = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
288
334
|
await this.saveProvider(answers);
|
|
289
335
|
} catch (error) {
|
|
290
336
|
// 移除 ESC 键监听
|
|
@@ -315,10 +361,12 @@ class ProviderAdder extends BaseCommand {
|
|
|
315
361
|
|
|
316
362
|
await this.configManager.addProvider(answers.name, {
|
|
317
363
|
displayName: answers.displayName || answers.name,
|
|
364
|
+
ideName: answers.ideName || 'claude',
|
|
318
365
|
baseUrl: answers.baseUrl,
|
|
319
366
|
authToken: answers.authToken,
|
|
320
367
|
authMode: answers.authMode,
|
|
321
368
|
tokenType: answers.tokenType, // 仅在 authMode 为 'api_key' 时使用
|
|
369
|
+
codexFiles: answers.codexFiles || null,
|
|
322
370
|
launchArgs,
|
|
323
371
|
primaryModel: modelConfig.primaryModel,
|
|
324
372
|
smallFastModel: modelConfig.smallFastModel,
|
|
@@ -472,6 +520,18 @@ class ProviderAdder extends BaseCommand {
|
|
|
472
520
|
console.log(chalk.gray(` 名称: ${answers.name}`));
|
|
473
521
|
console.log(chalk.gray(` 显示名称: ${finalDisplayName}`));
|
|
474
522
|
|
|
523
|
+
if (answers.ideName === 'codex') {
|
|
524
|
+
console.log(chalk.gray(' IDE: Codex CLI'));
|
|
525
|
+
if (answers.baseUrl) {
|
|
526
|
+
console.log(chalk.gray(` OPENAI_BASE_URL: ${answers.baseUrl}`));
|
|
527
|
+
}
|
|
528
|
+
if (answers.authToken) {
|
|
529
|
+
console.log(chalk.gray(` OPENAI_API_KEY: ${answers.authToken}`));
|
|
530
|
+
}
|
|
531
|
+
console.log(chalk.green('\n🎉 供应商添加完成!正在返回主界面...'));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
475
535
|
const authModeDisplay = {
|
|
476
536
|
api_key: '通用API密钥模式',
|
|
477
537
|
auth_token: '认证令牌模式 (仅 ANTHROPIC_AUTH_TOKEN)',
|
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) => {
|
|
@@ -35,39 +49,49 @@ class ProviderLister {
|
|
|
35
49
|
|
|
36
50
|
console.log(`${status} ${availabilityIcon} ${nameColor(provider.name)} (${provider.displayName}) - ${availabilityText}`);
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
api_key: '通用API密钥模式',
|
|
41
|
-
auth_token: '认证令牌模式',
|
|
42
|
-
oauth_token: 'OAuth令牌模式'
|
|
43
|
-
};
|
|
44
|
-
console.log(chalk.gray(` 认证模式: ${authModeDisplay[provider.authMode] || provider.authMode}`));
|
|
45
|
-
|
|
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 模式
|
|
52
|
+
if (provider.ideName === 'codex') {
|
|
53
|
+
console.log(chalk.gray(' IDE: Codex CLI'));
|
|
57
54
|
if (provider.baseUrl) {
|
|
58
|
-
console.log(chalk.gray(`
|
|
55
|
+
console.log(chalk.gray(` OPENAI_BASE_URL: ${provider.baseUrl}`));
|
|
59
56
|
}
|
|
60
57
|
if (provider.authToken) {
|
|
61
|
-
|
|
62
|
-
console.log(chalk.gray(` ${tokenEnvName}: ${provider.authToken}`));
|
|
58
|
+
console.log(chalk.gray(` OPENAI_API_KEY: ${provider.authToken}`));
|
|
63
59
|
}
|
|
64
60
|
} else {
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
// 显示认证模式
|
|
62
|
+
const authModeDisplay = {
|
|
63
|
+
api_key: '通用API密钥模式',
|
|
64
|
+
auth_token: '认证令牌模式',
|
|
65
|
+
oauth_token: 'OAuth令牌模式'
|
|
66
|
+
};
|
|
67
|
+
console.log(chalk.gray(` 认证模式: ${authModeDisplay[provider.authMode] || provider.authMode}`));
|
|
68
|
+
|
|
69
|
+
// 根据不同模式显示对应的环境变量名称
|
|
70
|
+
if (provider.authMode === 'oauth_token') {
|
|
71
|
+
// OAuth 模式
|
|
72
|
+
if (provider.authToken) {
|
|
73
|
+
console.log(chalk.gray(` CLAUDE_CODE_OAUTH_TOKEN: ${provider.authToken}`));
|
|
74
|
+
}
|
|
75
|
+
if (provider.baseUrl) {
|
|
76
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
|
|
77
|
+
}
|
|
78
|
+
} else if (provider.authMode === 'api_key') {
|
|
79
|
+
// API Key 模式
|
|
80
|
+
if (provider.baseUrl) {
|
|
81
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
|
|
82
|
+
}
|
|
83
|
+
if (provider.authToken) {
|
|
84
|
+
const tokenEnvName = provider.tokenType === 'auth_token' ? 'ANTHROPIC_AUTH_TOKEN' : 'ANTHROPIC_API_KEY';
|
|
85
|
+
console.log(chalk.gray(` ${tokenEnvName}: ${provider.authToken}`));
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// auth_token 模式
|
|
89
|
+
if (provider.baseUrl) {
|
|
90
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${provider.baseUrl}`));
|
|
91
|
+
}
|
|
92
|
+
if (provider.authToken) {
|
|
93
|
+
console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${provider.authToken}`));
|
|
94
|
+
}
|
|
71
95
|
}
|
|
72
96
|
}
|
|
73
97
|
|
|
@@ -132,9 +156,9 @@ class ProviderLister {
|
|
|
132
156
|
}
|
|
133
157
|
}
|
|
134
158
|
|
|
135
|
-
async function listCommand() {
|
|
159
|
+
async function listCommand(filter = null) {
|
|
136
160
|
const lister = new ProviderLister();
|
|
137
|
-
await lister.list();
|
|
161
|
+
await lister.list(filter);
|
|
138
162
|
}
|
|
139
163
|
|
|
140
164
|
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');
|
|
@@ -34,6 +35,10 @@ class EnvSwitcher extends BaseCommand {
|
|
|
34
35
|
try {
|
|
35
36
|
this.clearScreen();
|
|
36
37
|
const provider = await this.validateProvider(providerName);
|
|
38
|
+
if (provider.ideName === 'codex') {
|
|
39
|
+
// Codex CLI 使用环境变量注入方式启动,直接跳过参数选择
|
|
40
|
+
return await this.launchProvider(provider, provider.launchArgs || []);
|
|
41
|
+
}
|
|
37
42
|
const availableArgs = this.getAvailableLaunchArgs();
|
|
38
43
|
|
|
39
44
|
console.log(UIHelper.createTitle('启动配置', UIHelper.icons.launch));
|
|
@@ -198,15 +203,20 @@ class EnvSwitcher extends BaseCommand {
|
|
|
198
203
|
|
|
199
204
|
async launchProvider(provider, selectedLaunchArgs) {
|
|
200
205
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
const isCodex = provider.ideName === 'codex';
|
|
207
|
+
|
|
208
|
+
// Claude Code 才需要检测设置冲突
|
|
209
|
+
if (!isCodex) {
|
|
210
|
+
const shouldContinue = await this.ensureClaudeSettingsCompatibility(provider);
|
|
211
|
+
if (!shouldContinue) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
this.clearScreen();
|
|
208
217
|
console.log(UIHelper.createTitle('正在启动', UIHelper.icons.loading));
|
|
209
218
|
console.log();
|
|
219
|
+
const ideDisplayName = isCodex ? 'Codex CLI' : 'Claude Code';
|
|
210
220
|
console.log(UIHelper.createCard('目标供应商', UIHelper.formatProvider(provider), UIHelper.icons.launch));
|
|
211
221
|
|
|
212
222
|
if (selectedLaunchArgs.length > 0) {
|
|
@@ -228,11 +238,15 @@ class EnvSwitcher extends BaseCommand {
|
|
|
228
238
|
|
|
229
239
|
UIHelper.clearLoadingAnimation(loadingInterval);
|
|
230
240
|
|
|
231
|
-
console.log(UIHelper.createCard('准备就绪',
|
|
241
|
+
console.log(UIHelper.createCard('准备就绪', `环境配置完成,正在启动 🚀 ${ideDisplayName}...`, UIHelper.icons.success));
|
|
232
242
|
console.log();
|
|
233
243
|
|
|
234
|
-
|
|
235
|
-
|
|
244
|
+
if (isCodex) {
|
|
245
|
+
await executeCodexWithEnv(provider, selectedLaunchArgs);
|
|
246
|
+
} else {
|
|
247
|
+
// 设置环境变量并启动 Claude Code
|
|
248
|
+
await executeWithEnv(provider, selectedLaunchArgs);
|
|
249
|
+
}
|
|
236
250
|
|
|
237
251
|
} catch (error) {
|
|
238
252
|
UIHelper.clearLoadingAnimation(loadingInterval);
|
|
@@ -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 };
|