@pikecode/api-key-manager 1.0.38 → 1.0.42

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.
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Provider Validation Command
3
+ * 验证供应商配置的有效性
4
+ * @module commands/validate
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const { configManager } = require('../config');
9
+ const { Logger } = require('../utils/logger');
10
+ const { ProviderStatusChecker } = require('../utils/provider-status-checker');
11
+ const { UIHelper } = require('../utils/ui-helper');
12
+
13
+ /**
14
+ * 供应商验证器
15
+ * 用于验证 API 供应商的 Token 和配置是否有效
16
+ */
17
+ class ProviderValidator {
18
+ /**
19
+ * 创建供应商验证器实例
20
+ */
21
+ constructor() {
22
+ this.configManager = configManager;
23
+ this.statusChecker = new ProviderStatusChecker();
24
+ }
25
+
26
+ /**
27
+ * 验证单个供应商
28
+ * @param {string} providerName - 供应商名称或别名
29
+ * @returns {Promise<void>}
30
+ */
31
+ async validateOne(providerName) {
32
+ try {
33
+ await this.configManager.ensureLoaded();
34
+
35
+ // 支持别名查找
36
+ let provider = this.configManager.getProvider(providerName);
37
+ if (!provider) {
38
+ provider = this.configManager.getProviderByNameOrAlias(providerName);
39
+ }
40
+
41
+ if (!provider) {
42
+ Logger.error(`供应商 '${providerName}' 不存在`);
43
+ Logger.info('使用 "akm list" 查看所有已配置的供应商');
44
+ return;
45
+ }
46
+
47
+ console.log(chalk.blue(`\n🔍 正在验证供应商: ${provider.displayName} (${provider.name})`));
48
+ console.log(chalk.gray('═'.repeat(60)));
49
+
50
+ const loadingInterval = UIHelper.createLoadingAnimation('验证中...');
51
+
52
+ try {
53
+ const status = await this.statusChecker.check(provider, { skipCache: true });
54
+ UIHelper.clearLoadingAnimation(loadingInterval);
55
+
56
+ this._printValidationResult(provider, status);
57
+ } catch (error) {
58
+ UIHelper.clearLoadingAnimation(loadingInterval);
59
+ Logger.error(`验证失败: ${error.message}`);
60
+ }
61
+
62
+ } catch (error) {
63
+ Logger.error(`验证供应商失败: ${error.message}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 验证所有供应商
69
+ * @param {string|null} filter - 过滤器 ('codex', 'claude', 或 null 表示全部)
70
+ * @returns {Promise<void>}
71
+ */
72
+ async validateAll(filter = null) {
73
+ try {
74
+ await this.configManager.ensureLoaded();
75
+ let providers = this.configManager.listProviders();
76
+
77
+ // 应用过滤器
78
+ if (filter === 'codex') {
79
+ providers = providers.filter(p => p.ideName === 'codex');
80
+ } else if (filter === 'claude') {
81
+ providers = providers.filter(p => p.ideName !== 'codex');
82
+ }
83
+
84
+ if (providers.length === 0) {
85
+ if (filter) {
86
+ const filterName = filter === 'codex' ? 'Codex CLI' : 'Claude Code';
87
+ Logger.warning(`暂无 ${filterName} 供应商配置`);
88
+ } else {
89
+ Logger.warning('暂无配置的供应商');
90
+ }
91
+ Logger.info('请使用 "akm add" 添加供应商配置');
92
+ return;
93
+ }
94
+
95
+ const titleSuffix = filter === 'codex' ? ' (Codex CLI)' : (filter === 'claude' ? ' (Claude Code)' : '');
96
+ console.log(chalk.blue(`\n🔍 正在验证供应商配置${titleSuffix}...`));
97
+ console.log(chalk.gray('═'.repeat(60)));
98
+ console.log();
99
+
100
+ let completedCount = 0;
101
+ const total = providers.length;
102
+
103
+ // 创建进度显示
104
+ const progressInterval = setInterval(() => {
105
+ process.stdout.write(`\r验证中... ${completedCount}/${total}`);
106
+ }, 100);
107
+
108
+ try {
109
+ // 使用流式检查,实时显示结果
110
+ const results = await this.statusChecker.checkAllStreaming(providers, (providerName, status) => {
111
+ completedCount++;
112
+ });
113
+
114
+ clearInterval(progressInterval);
115
+ process.stdout.write('\r' + ' '.repeat(50) + '\r'); // 清除进度显示
116
+
117
+ // 显示结果
118
+ this._printBatchResults(providers, results);
119
+ } catch (error) {
120
+ clearInterval(progressInterval);
121
+ Logger.error(`批量验证失败: ${error.message}`);
122
+ }
123
+
124
+ } catch (error) {
125
+ Logger.error(`验证供应商失败: ${error.message}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * 打印单个验证结果
131
+ * @private
132
+ * @param {Object} provider - 供应商配置
133
+ * @param {Object} status - 状态结果
134
+ */
135
+ _printValidationResult(provider, status) {
136
+ const stateIcon = this._getStateIcon(status.state);
137
+ const stateLabel = this._getStateLabel(status.state);
138
+ const stateColor = this._getStateColor(status.state);
139
+
140
+ console.log();
141
+ console.log(`${stateIcon} 状态: ${stateColor(stateLabel)}`);
142
+
143
+ if (status.message) {
144
+ console.log(` 消息: ${status.message}`);
145
+ }
146
+
147
+ if (status.latency !== null) {
148
+ console.log(` 响应时间: ${status.latency.toFixed(0)}ms`);
149
+ }
150
+
151
+ console.log();
152
+
153
+ // 配置详情
154
+ console.log(chalk.gray('配置详情:'));
155
+ console.log(chalk.gray(` 供应商名称: ${provider.name}`));
156
+ console.log(chalk.gray(` 显示名称: ${provider.displayName}`));
157
+ if (provider.alias) {
158
+ console.log(chalk.gray(` 别名: ${provider.alias}`));
159
+ }
160
+
161
+ if (provider.ideName === 'codex') {
162
+ console.log(chalk.gray(' IDE: Codex CLI'));
163
+ if (provider.baseUrl) {
164
+ console.log(chalk.gray(` OPENAI_BASE_URL: ${provider.baseUrl}`));
165
+ }
166
+ } else {
167
+ console.log(chalk.gray(` IDE: Claude Code`));
168
+ console.log(chalk.gray(` 认证模式: ${provider.authMode}`));
169
+ if (provider.baseUrl) {
170
+ console.log(chalk.gray(` 基础URL: ${provider.baseUrl}`));
171
+ }
172
+ }
173
+
174
+ // 根据状态提供建议
175
+ if (status.state === 'offline') {
176
+ console.log();
177
+ console.log(chalk.yellow('💡 建议:'));
178
+ console.log(chalk.yellow(' 1. 检查 Token 是否正确'));
179
+ console.log(chalk.yellow(' 2. 检查基础URL是否可访问'));
180
+ console.log(chalk.yellow(' 3. 检查网络连接是否正常'));
181
+ } else if (status.state === 'unknown') {
182
+ console.log();
183
+ console.log(chalk.yellow('💡 建议:'));
184
+ console.log(chalk.yellow(' 1. 检查配置是否完整'));
185
+ console.log(chalk.yellow(' 2. 使用 "akm edit" 更新配置'));
186
+ }
187
+ }
188
+
189
+ /**
190
+ * 打印批量验证结果
191
+ * @private
192
+ * @param {Array} providers - 供应商列表
193
+ * @param {Object} results - 验证结果
194
+ */
195
+ _printBatchResults(providers, results) {
196
+ const currentProvider = this.configManager.getCurrentProvider();
197
+
198
+ let onlineCount = 0;
199
+ let offlineCount = 0;
200
+ let unknownCount = 0;
201
+
202
+ providers.forEach(provider => {
203
+ const status = results[provider.name];
204
+ const isCurrent = provider.name === currentProvider?.name;
205
+ const statusIcon = isCurrent ? '✅' : '🔹';
206
+ const stateIcon = this._getStateIcon(status.state);
207
+ const stateColor = this._getStateColor(status.state);
208
+ const nameColor = isCurrent ? chalk.green : chalk.white;
209
+
210
+ // IDE 类型标签
211
+ const ideTag = provider.ideName === 'codex'
212
+ ? chalk.cyan('[Codex]')
213
+ : chalk.magenta('[Claude]');
214
+
215
+ // 别名
216
+ const aliasText = provider.alias ? chalk.yellow(` [别名: ${provider.alias}]`) : '';
217
+
218
+ const statusText = status.message || this._getStateLabel(status.state);
219
+
220
+ console.log(`${statusIcon} ${stateIcon} ${ideTag} ${nameColor(provider.name)} (${provider.displayName})${aliasText}`);
221
+ console.log(` ${stateColor(statusText)}`);
222
+
223
+ // 统计
224
+ if (status.state === 'online') onlineCount++;
225
+ else if (status.state === 'offline') offlineCount++;
226
+ else unknownCount++;
227
+ });
228
+
229
+ // 显示统计
230
+ console.log();
231
+ console.log(chalk.gray('═'.repeat(60)));
232
+ console.log(chalk.blue('📊 验证统计:'));
233
+ console.log(` 总计: ${providers.length} 个供应商`);
234
+ console.log(` ${chalk.green('✓')} 可用: ${onlineCount}`);
235
+ console.log(` ${chalk.red('✗')} 不可用: ${offlineCount}`);
236
+ console.log(` ${chalk.yellow('?')} 未知: ${unknownCount}`);
237
+ }
238
+
239
+ /**
240
+ * 获取状态图标
241
+ * @private
242
+ * @param {string} state - 状态
243
+ * @returns {string} 图标
244
+ */
245
+ _getStateIcon(state) {
246
+ switch (state) {
247
+ case 'online': return '✓';
248
+ case 'offline': return '✗';
249
+ case 'degraded': return '⚠';
250
+ case 'pending': return '⋯';
251
+ default: return '?';
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 获取状态标签
257
+ * @private
258
+ * @param {string} state - 状态
259
+ * @returns {string} 标签
260
+ */
261
+ _getStateLabel(state) {
262
+ switch (state) {
263
+ case 'online': return '可用';
264
+ case 'offline': return '不可用';
265
+ case 'degraded': return '降级';
266
+ case 'pending': return '检测中';
267
+ default: return '未知';
268
+ }
269
+ }
270
+
271
+ /**
272
+ * 获取状态颜色
273
+ * @private
274
+ * @param {string} state - 状态
275
+ * @returns {Function} 颜色函数
276
+ */
277
+ _getStateColor(state) {
278
+ switch (state) {
279
+ case 'online': return chalk.green;
280
+ case 'offline': return chalk.red;
281
+ case 'degraded': return chalk.yellow;
282
+ case 'pending': return chalk.blue;
283
+ default: return chalk.gray;
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * 验证命令
290
+ * @param {string|null} providerName - 供应商名称或别名
291
+ * @param {Object} options - 选项
292
+ * @param {string|null} options.filter - 过滤器
293
+ * @returns {Promise<void>}
294
+ */
295
+ async function validateCommand(providerName, options = {}) {
296
+ const validator = new ProviderValidator();
297
+
298
+ try {
299
+ if (providerName) {
300
+ await validator.validateOne(providerName);
301
+ } else {
302
+ await validator.validateAll(options.filter || null);
303
+ }
304
+ } catch (error) {
305
+ Logger.error(`验证失败: ${error.message}`);
306
+ throw error;
307
+ }
308
+ }
309
+
310
+ module.exports = { validateCommand, ProviderValidator };
package/src/config.js CHANGED
@@ -3,15 +3,54 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const chalk = require('chalk');
5
5
 
6
+ /**
7
+ * @typedef {Object} ProviderConfig
8
+ * @property {string} name - 供应商名称
9
+ * @property {string} displayName - 显示名称
10
+ * @property {string} ideName - IDE 名称 ('claude' 或 'codex')
11
+ * @property {string} authMode - 认证模式
12
+ * @property {string} authToken - 认证令牌
13
+ * @property {string|null} baseUrl - API 基础 URL
14
+ * @property {string|null} tokenType - Token 类型
15
+ * @property {Object|null} models - 模型配置
16
+ * @property {string[]} launchArgs - 启动参数
17
+ * @property {boolean} current - 是否为当前供应商
18
+ * @property {number} usageCount - 使用次数
19
+ * @property {string} lastUsed - 最后使用时间
20
+ * @property {string} createdAt - 创建时间
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} Config
25
+ * @property {string} version - 配置版本
26
+ * @property {string|null} currentProvider - 当前供应商名称
27
+ * @property {Object.<string, ProviderConfig>} providers - 供应商配置对象
28
+ */
29
+
30
+ /**
31
+ * 配置管理器
32
+ * 管理 API 供应商配置的加载、保存和操作
33
+ */
6
34
  class ConfigManager {
7
35
  constructor() {
36
+ /** @type {string} 配置文件路径 */
8
37
  this.configPath = path.join(os.homedir(), '.akm-config.json');
9
- this.config = null; // 延迟加载
38
+ /** @type {Config|null} 配置数据 */
39
+ this.config = null;
40
+ /** @type {boolean} 是否已加载 */
10
41
  this.isLoaded = false;
42
+ /** @type {Date|null} 最后修改时间 */
11
43
  this.lastModified = null;
12
- this.loadPromise = null; // 防止并发加载
44
+ /** @type {Promise<Config>|null} 加载 Promise,防止并发加载 */
45
+ this.loadPromise = null;
13
46
  }
14
47
 
48
+ /**
49
+ * 标准化可选字符串值
50
+ * @private
51
+ * @param {*} value - 输入值
52
+ * @returns {string|null} 标准化后的值
53
+ */
15
54
  _normalizeOptionalString(value) {
16
55
  if (value === null || value === undefined) {
17
56
  return null;
@@ -23,6 +62,10 @@ class ConfigManager {
23
62
  return trimmed.length === 0 ? null : trimmed;
24
63
  }
25
64
 
65
+ /**
66
+ * 获取默认配置
67
+ * @returns {Config} 默认配置对象
68
+ */
26
69
  getDefaultConfig() {
27
70
  return {
28
71
  version: '1.0.0',
@@ -31,6 +74,11 @@ class ConfigManager {
31
74
  };
32
75
  }
33
76
 
77
+ /**
78
+ * 加载配置文件
79
+ * @param {boolean} [forceReload=false] - 是否强制重新加载
80
+ * @returns {Promise<Config>} 配置对象
81
+ */
34
82
  async load(forceReload = false) {
35
83
  // 如果正在加载,等待当前加载完成
36
84
  if (this.loadPromise) {
@@ -248,14 +296,21 @@ class ConfigManager {
248
296
  ? providerConfig.launchArgs
249
297
  : (existing?.launchArgs || []);
250
298
 
299
+ // 处理别名
300
+ const alias = providerConfig.alias !== undefined
301
+ ? this._normalizeOptionalString(providerConfig.alias)
302
+ : (existing?.alias || null);
303
+
251
304
  // 基础字段
252
305
  this.config.providers[name] = {
253
306
  name,
254
307
  displayName: providerConfig.displayName || existing?.displayName || name,
308
+ alias,
255
309
  ideName,
256
310
  baseUrl,
257
311
  authToken,
258
312
  launchArgs,
313
+ lastUsedArgs: existing?.lastUsedArgs || null,
259
314
  createdAt: existing?.createdAt || now,
260
315
  lastUsed: existing?.lastUsed || now,
261
316
  usageCount: existing?.usageCount || 0,
@@ -338,6 +393,168 @@ class ConfigManager {
338
393
  return await this.save();
339
394
  }
340
395
 
396
+ /**
397
+ * 更新供应商的上次使用启动参数
398
+ * @param {string} name - 供应商名称
399
+ * @param {string[]} args - 启动参数数组
400
+ * @returns {Promise<void>}
401
+ */
402
+ async updateLastUsedArgs(name, args) {
403
+ await this.ensureLoaded();
404
+
405
+ if (!this.config.providers[name]) {
406
+ throw new Error(`供应商 '${name}' 不存在`);
407
+ }
408
+
409
+ // 更新上次使用的启动参数
410
+ this.config.providers[name].lastUsedArgs = args;
411
+ this.config.providers[name].lastUsed = new Date().toISOString();
412
+
413
+ // 增加使用次数
414
+ this.config.providers[name].usageCount = (this.config.providers[name].usageCount || 0) + 1;
415
+
416
+ return await this.save();
417
+ }
418
+
419
+ /**
420
+ * 记录供应商使用会话
421
+ * @param {string} name - 供应商名称
422
+ * @param {number} durationMs - 使用时长(毫秒)
423
+ */
424
+ async recordUsageSession(name, durationMs = 0) {
425
+ await this.ensureLoaded();
426
+
427
+ if (!this.config.providers[name]) {
428
+ throw new Error(`供应商 '${name}' 不存在`);
429
+ }
430
+
431
+ const provider = this.config.providers[name];
432
+
433
+ // 初始化统计数据
434
+ if (!provider.stats) {
435
+ provider.stats = {
436
+ totalSessions: 0,
437
+ totalDurationMs: 0,
438
+ averageDurationMs: 0,
439
+ lastSessionDuration: 0,
440
+ firstUsed: new Date().toISOString()
441
+ };
442
+ }
443
+
444
+ // 更新统计
445
+ provider.stats.totalSessions = (provider.stats.totalSessions || 0) + 1;
446
+ provider.stats.totalDurationMs = (provider.stats.totalDurationMs || 0) + durationMs;
447
+ provider.stats.lastSessionDuration = durationMs;
448
+ provider.stats.averageDurationMs = Math.round(
449
+ provider.stats.totalDurationMs / provider.stats.totalSessions
450
+ );
451
+
452
+ // 更新最后使用时间
453
+ provider.lastUsed = new Date().toISOString();
454
+
455
+ return await this.save();
456
+ }
457
+
458
+ /**
459
+ * 获取使用统计信息
460
+ * @param {string|null} name - 供应商名称,null 表示获取所有
461
+ * @returns {Object} 统计信息
462
+ */
463
+ getUsageStats(name = null) {
464
+ if (!this.isLoaded) {
465
+ throw new Error('配置未加载,请先调用 load() 方法');
466
+ }
467
+
468
+ if (name) {
469
+ const provider = this.getProvider(name);
470
+ if (!provider) {
471
+ return null;
472
+ }
473
+ return {
474
+ name: provider.name,
475
+ displayName: provider.displayName,
476
+ usageCount: provider.usageCount || 0,
477
+ lastUsed: provider.lastUsed || null,
478
+ stats: provider.stats || null
479
+ };
480
+ }
481
+
482
+ // 返回所有供应商的统计
483
+ const allStats = Object.entries(this.config.providers).map(([name, provider]) => ({
484
+ name,
485
+ displayName: provider.displayName,
486
+ usageCount: provider.usageCount || 0,
487
+ lastUsed: provider.lastUsed || null,
488
+ stats: provider.stats || null
489
+ }));
490
+
491
+ // 按使用次数降序排序
492
+ return allStats.sort((a, b) => (b.usageCount || 0) - (a.usageCount || 0));
493
+ }
494
+
495
+ /**
496
+ * 获取智能推荐的供应商列表
497
+ * @param {Object} options - 选项
498
+ * @param {number} options.limit - 返回数量限制
499
+ * @param {string|null} options.filter - 过滤器 ('codex', 'claude', 或 null)
500
+ * @returns {Array} 推荐的供应商列表
501
+ */
502
+ getRecommendedProviders(options = {}) {
503
+ if (!this.isLoaded) {
504
+ throw new Error('配置未加载,请先调用 load() 方法');
505
+ }
506
+
507
+ const { limit = 5, filter = null } = options;
508
+ let providers = this.listProviders();
509
+
510
+ // 应用过滤器
511
+ if (filter === 'codex') {
512
+ providers = providers.filter(p => p.ideName === 'codex');
513
+ } else if (filter === 'claude') {
514
+ providers = providers.filter(p => p.ideName !== 'codex');
515
+ }
516
+
517
+ // 计算推荐分数
518
+ const scoredProviders = providers.map(provider => {
519
+ let score = 0;
520
+
521
+ // 使用次数权重 (40%)
522
+ const usageCount = provider.usageCount || 0;
523
+ score += usageCount * 0.4;
524
+
525
+ // 最近使用时间权重 (30%)
526
+ if (provider.lastUsed) {
527
+ const daysSinceLastUse = (Date.now() - new Date(provider.lastUsed).getTime()) / (1000 * 60 * 60 * 24);
528
+ // 越近使用分数越高,超过30天分数衰减
529
+ const recencyScore = Math.max(0, 30 - daysSinceLastUse) / 30;
530
+ score += recencyScore * 30;
531
+ }
532
+
533
+ // 会话平均时长权重 (20%)
534
+ if (provider.stats?.averageDurationMs) {
535
+ // 平均使用时长越长,说明使用越频繁,最高20分
536
+ const avgMinutes = provider.stats.averageDurationMs / (1000 * 60);
537
+ const durationScore = Math.min(avgMinutes / 60, 1) * 20; // 最多1小时算满分
538
+ score += durationScore;
539
+ }
540
+
541
+ // 总会话数权重 (10%)
542
+ if (provider.stats?.totalSessions) {
543
+ score += Math.min(provider.stats.totalSessions / 10, 1) * 10;
544
+ }
545
+
546
+ return {
547
+ ...provider,
548
+ recommendScore: Math.round(score * 100) / 100
549
+ };
550
+ });
551
+
552
+ // 按推荐分数降序排序并限制数量
553
+ return scoredProviders
554
+ .sort((a, b) => b.recommendScore - a.recommendScore)
555
+ .slice(0, limit);
556
+ }
557
+
341
558
  getProvider(name) {
342
559
  // 同步方法,但需要先确保配置已加载
343
560
  if (!this.isLoaded) {
@@ -346,6 +563,30 @@ class ConfigManager {
346
563
  return this.config.providers[name];
347
564
  }
348
565
 
566
+ /**
567
+ * 通过名称或别名获取供应商
568
+ * @param {string} nameOrAlias - 供应商名称或别名
569
+ * @returns {ProviderConfig|null} 供应商配置对象,未找到返回 null
570
+ */
571
+ getProviderByNameOrAlias(nameOrAlias) {
572
+ // 同步方法,但需要先确保配置已加载
573
+ if (!this.isLoaded) {
574
+ throw new Error('配置未加载,请先调用 load() 方法');
575
+ }
576
+
577
+ // 先尝试按名称查找
578
+ if (this.config.providers[nameOrAlias]) {
579
+ return this.config.providers[nameOrAlias];
580
+ }
581
+
582
+ // 再尝试按别名查找
583
+ const providerEntry = Object.entries(this.config.providers).find(
584
+ ([_, provider]) => provider.alias && provider.alias.toLowerCase() === nameOrAlias.toLowerCase()
585
+ );
586
+
587
+ return providerEntry ? providerEntry[1] : null;
588
+ }
589
+
349
590
  listProviders() {
350
591
  // 同步方法,但需要先确保配置已加载
351
592
  if (!this.isLoaded) {